mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-26 16:28:52 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dab1303c18 | |||
| 0daceb74e0 | |||
| 7592809527 | |||
| 7b8efedf7c | |||
| 6ab6171ec6 | |||
| 8fea39665b | |||
| a6632c4f35 | |||
| f6d8ffd3e5 | |||
| 25e1590be4 |
@@ -1,51 +0,0 @@
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: bepsays/ci-goreleaser:1.1800.300
|
||||
environment:
|
||||
CGO_ENABLED: "0"
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout:
|
||||
path: hugo
|
||||
- run:
|
||||
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:
|
||||
- attach_workspace:
|
||||
at: /root/project
|
||||
- run:
|
||||
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:
|
||||
- build:
|
||||
filters:
|
||||
branches:
|
||||
only: /release-.*/
|
||||
- hold:
|
||||
type: approval
|
||||
requires:
|
||||
- build
|
||||
- release:
|
||||
context: org-global
|
||||
requires:
|
||||
- hold
|
||||
@@ -1,9 +0,0 @@
|
||||
*.md
|
||||
*.log
|
||||
*.txt
|
||||
.git
|
||||
.github
|
||||
.circleci
|
||||
docs
|
||||
examples
|
||||
Dockerfile
|
||||
@@ -1,8 +0,0 @@
|
||||
# Text files have auto line endings
|
||||
* text=auto
|
||||
|
||||
# Go source files always have LF line endings
|
||||
*.go text eol=lf
|
||||
|
||||
# SVG files should not be modified
|
||||
*.svg -text
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: 'Bug report'
|
||||
labels: 'Bug, NeedsTriage'
|
||||
assignees: ''
|
||||
about: Create a report to help us improve
|
||||
---
|
||||
|
||||
|
||||
<!-- Please answer these questions before submitting your issue. Thanks! -->
|
||||
|
||||
### What version of Hugo are you using (`hugo version`)?
|
||||
|
||||
<pre>
|
||||
$ hugo version
|
||||
|
||||
</pre>
|
||||
|
||||
### Does this issue reproduce with the latest release?
|
||||
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: SUPPORT, ISSUES and TROUBLESHOOTING
|
||||
url: https://discourse.gohugo.io/
|
||||
about: Please DO NOT use Github for support requests. Please visit https://discourse.gohugo.io for support! You will be helped much faster there. If you ignore this request your issue might be closed with a discourse label.
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
name: Proposal
|
||||
about: Propose a new feature for Hugo
|
||||
title: ''
|
||||
labels: 'Proposal, NeedsTriage'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
<!-- Describe this new feature. Think about if it really belongs in the Hugo core module; you may want to discuss it on https://discourse.gohugo.io/ first. -->
|
||||
@@ -1,3 +0,0 @@
|
||||
### Asking Support Questions
|
||||
|
||||
We have an active [discussion forum](https://discourse.gohugo.io) where users and developers can ask questions. Please don't use the GitHub issue tracker to ask questions.
|
||||
@@ -1,7 +0,0 @@
|
||||
# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#package-ecosystem
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -1,47 +0,0 @@
|
||||
name: 'Close stale and lock closed issues and PRs'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@08e671be8ac8944d0e132aa71d0ae8ccfb347675
|
||||
with:
|
||||
issue-inactive-days: 21
|
||||
add-issue-labels: 'Outdated'
|
||||
issue-comment: >
|
||||
This issue has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
pr-comment: >
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
- uses: actions/stale@04a1828bc18ada028d85a0252a47cd2963a91abe
|
||||
with:
|
||||
operations-per-run: 999
|
||||
days-before-issue-stale: 365
|
||||
days-before-pr-stale: 365
|
||||
days-before-issue-close: 56
|
||||
days-before-pr-close: 56
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
|
||||
|
||||
If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
|
||||
|
||||
If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
|
||||
|
||||
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
stale-pr-message: This PR has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
|
||||
|
||||
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell is in a comment.
|
||||
|
||||
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
stale-issue-label: 'Stale'
|
||||
exempt-issue-labels: 'Keep,Security'
|
||||
stale-pr-label: 'Stale'
|
||||
exempt-pr-labels: 'Keep,Security'
|
||||
@@ -1,90 +0,0 @@
|
||||
on: [push, pull_request]
|
||||
name: Test
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
strategy:
|
||||
matrix:
|
||||
# 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: Install Go
|
||||
uses: actions/setup-go@37335c7bb261b353407cff977110895fa0b4f7d8
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Install Ruby
|
||||
uses: actions/setup-ruby@5f29a1cd8dfebf420691c4c9a0e832e2fae5a526
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install Mage
|
||||
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
|
||||
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 pandoc
|
||||
choco install mingw --version 10.2.0 --allow-downgrade
|
||||
- run: pandoc -v
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install dart-sass-embedded Linux
|
||||
run: |
|
||||
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-embedded MacOS
|
||||
run: |
|
||||
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-embedded Windows
|
||||
run: |
|
||||
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: |
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
- name: Build Docs
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
HUGO_TIMEOUT: 31000
|
||||
HUGO_IGNOREERRORS: error-remote-getjson
|
||||
HUGO_SERVICES_INSTAGRAM_ACCESSTOKEN: dummytoken
|
||||
run: |
|
||||
mage -v hugo
|
||||
./hugo -s docs/
|
||||
./hugo --renderToMemory -s docs/
|
||||
+13
-25
@@ -1,29 +1,17 @@
|
||||
/hugo
|
||||
docs/public*
|
||||
/.idea
|
||||
.vscode/*
|
||||
hugo.exe
|
||||
*.test
|
||||
*.prof
|
||||
nohup.out
|
||||
cover.out
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
*~
|
||||
hugo
|
||||
docs/public*
|
||||
/.idea
|
||||
hugo.exe
|
||||
*.test
|
||||
*.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
|
||||
GoBuilds
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ArtifactsDest": "GoBuilds/",
|
||||
"OutPath": "{{.Dest}}{{.PS}}{{.AppName}}{{.PS}}{{.Version}}{{.PS}}{{.AppName}}_{{.Version}}_{{.Os}}_{{.Arch}}{{.Ext}}",
|
||||
"BuildConstraints": "linux windows darwin freebsd netbsd openbsd dragonfly",
|
||||
"ConfigVersion": "0.9"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
language: go
|
||||
sudo: required
|
||||
go:
|
||||
- 1.6.3
|
||||
- 1.7.3
|
||||
- tip
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
fast_finish: true
|
||||
install:
|
||||
- make govendor
|
||||
script:
|
||||
- make check
|
||||
# Test 64-bit alignment on 32-bit builds
|
||||
- env GOARCH=386 make test
|
||||
- go build -race
|
||||
- ./hugo -s docs/
|
||||
- ./hugo --renderToMemory -s docs/
|
||||
before_install:
|
||||
# gem install must be run with sudo on OSX
|
||||
- sudo gem install asciidoctor | gem install asciidoctor
|
||||
- sudo pip install docutils
|
||||
+55
-105
@@ -1,17 +1,13 @@
|
||||
# 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,
|
||||
helping to manage issues, etc.
|
||||
|
||||
The Hugo community and maintainers are [very active](https://github.com/gohugoio/hugo/pulse/monthly) and helpful, and the project benefits greatly from this activity. We created a [step by step guide](https://gohugo.io/tutorials/how-to-contribute-to-hugo/) if you're unfamiliar with GitHub or contributing to open source projects in general.
|
||||
The Hugo community and maintainers are very active and helpful, and the project benefits greatly from this activity. We created a [step by step guide](https://gohugo.io/tutorials/how-to-contribute-to-hugo/) if you're unfamiliar with GitHub or contributing to open source projects in general.
|
||||
|
||||
*Note that this repository only contains the actual source code of Hugo. For **only** documentation-related pull requests / issues please refer to the [hugoDocs](https://github.com/gohugoio/hugoDocs) repository.*
|
||||
|
||||
*Changes to the codebase **and** related documentation, e.g. for a new feature, should still use a single pull request.*
|
||||
[](https://waffle.io/spf13/hugo/metrics)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -20,85 +16,61 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
|
||||
* [Submitting Patches](#submitting-patches)
|
||||
* [Code Contribution Guidelines](#code-contribution-guidelines)
|
||||
* [Git Commit Message Guidelines](#git-commit-message-guidelines)
|
||||
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
|
||||
* [Building Hugo with Your Changes](#building-hugo-with-your-changes)
|
||||
* [Using Git Remotes](#using-git-remotes)
|
||||
* [Build Hugo with Your Changes](#build-hugo-with-your-changes)
|
||||
* [Add Compile Information to Hugo](#add-compile-information-to-hugo)
|
||||
|
||||
## 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.
|
||||
We have an active [discussion forum](http://discuss.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`) and your operating system.
|
||||
|
||||
- [Hugo Issues · gohugoio/hugo](https://github.com/gohugoio/hugo/issues)
|
||||
- [Hugo Documentation Issues · gohugoio/hugoDocs](https://github.com/gohugoio/hugoDocs/issues)
|
||||
- [Hugo Website Theme Issues · gohugoio/hugoThemesSite](https://github.com/gohugoio/hugoThemesSite/issues)
|
||||
|
||||
## Code Contribution
|
||||
|
||||
Hugo has become a fully featured static site generator, so any new functionality must:
|
||||
|
||||
* be useful to many.
|
||||
* fit naturally into _what Hugo does best._
|
||||
* strive not to break existing sites.
|
||||
* close or update an open [Hugo issue](https://github.com/gohugoio/hugo/issues)
|
||||
|
||||
If it is of some complexity, the contributor is expected to maintain and support the new feature in the future (answer questions on the forum, fix any bugs etc.).
|
||||
|
||||
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).
|
||||
We have one exception to this rule which is LibSASS.
|
||||
|
||||
**Bug fixes are, of course, always welcome.**
|
||||
the Github [issue tracker](https://github.com/spf13/hugo/issues) to report the problem to the Hugo maintainers.
|
||||
If you're not sure if it's a bug or not, start by asking in the [discussion forum](http://discuss.gohugo.io).
|
||||
When reporting the issue, please provide the version of Hugo in use (`hugo version`) and your operating system.
|
||||
|
||||
## Submitting Patches
|
||||
|
||||
The Hugo project welcomes all contributors and contributions regardless of skill or experience level. If you are interested in helping with the project, we will help you with your contribution.
|
||||
The Hugo project welcomes all contributors and contributions regardless of skill or experience level.
|
||||
If you are interested in helping with the project, we will help you with your contribution.
|
||||
Hugo is a very active project with many contributions happening daily.
|
||||
Because we want to create the best possible product for our users and the best contribution experience for our developers,
|
||||
we have a set of guidelines which ensure that all contributions are acceptable.
|
||||
The guidelines are not intended as a filter or barrier to participation.
|
||||
If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines.
|
||||
|
||||
### Code Contribution Guidelines
|
||||
|
||||
Because we want to create the best possible product for our users and the best contribution experience for our developers, we have a set of guidelines which ensure that all contributions are acceptable. The guidelines are not intended as a filter or barrier to participation. If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines.
|
||||
|
||||
To make the contribution process as seamless as possible, we ask for the following:
|
||||
|
||||
* Go ahead and fork the project and make your changes. We encourage pull requests to allow for review and discussion of code changes.
|
||||
* When you’re ready to create a pull request, be sure to:
|
||||
* Sign the [CLA](https://cla-assistant.io/gohugoio/hugo).
|
||||
* Sign the [CLA](https://cla-assistant.io/spf13/hugo).
|
||||
* Have test cases for the new code. If you have questions about how to do this, please ask in your pull request.
|
||||
* Run `go fmt`.
|
||||
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
|
||||
* Squash your commits into a single commit. `git rebase -i`. It’s okay to force update your pull request with `git push -f`.
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
|
||||
* Make sure `go test ./...` passes, and `go build` completes. [Travis CI](https://travis-ci.org/spf13/hugo) (Linux and OS X) and [AppVeyor](https://ci.appveyor.com/project/spf13/hugo/branch/master) (Windows) will catch most things that are missing.
|
||||
* Follow the **Git Commit Message Guidelines** below.
|
||||
|
||||
### Git Commit Message Guidelines
|
||||
|
||||
This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages,
|
||||
This [blog article](http://chris.beams.io/posts/git-commit/) is a good resource for learning how to write good commit messages,
|
||||
the most important part being that each commit message should have a title/subject in imperative mood starting with a capital letter and no trailing period:
|
||||
*"js: Return error when option x is not set"*, **NOT** *"returning some error."*
|
||||
|
||||
Most title/subjects should have a lower-cased prefix with a colon and one whitespace. The prefix can be:
|
||||
|
||||
* The name of the package where (most of) the changes are made (e.g. `media: Add text/calendar`)
|
||||
* If the package name is deeply nested/long, try to shorten it from the left side, e.g. `markup/goldmark` is OK, `resources/resource_transformers/js` can be shortened to `js`.
|
||||
* If this commit touches several packages with a common functional topic, use that as a prefix, e.g. `errors: Resolve correct line numbers`)
|
||||
* If this commit touches many packages without a common functional topic, prefix with `all:` (e.g. `all: Reformat Go code`)
|
||||
* If this is a documentation update, prefix with `docs:`.
|
||||
* If nothing of the above applies, just leave the prefix out.
|
||||
*"Return error on wrong use of the Paginator"*, **NOT** *"returning some error."*
|
||||
|
||||
Also, if your commit references one or more GitHub issues, always end your commit message body with *See #1234* or *Fixes #1234*.
|
||||
Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*.
|
||||
|
||||
Sometimes it makes sense to prefix the commit message with the packagename (or docs folder) all lowercased ending with a colon.
|
||||
That is fine, but the rest of the rules above apply.
|
||||
So it is "tpl: Add emojify template func", not "tpl: add emojify template func.", and "docs: Document emoji", not "doc: document emoji."
|
||||
|
||||
Please consider to use a short and descriptive branch name, e.g. **NOT** "patch-1". It's very common but creates a naming conflict each time when a submission is pulled for a review.
|
||||
|
||||
An example:
|
||||
|
||||
```text
|
||||
@@ -112,91 +84,69 @@ new default function more useful for Hugo users.
|
||||
Fixes #1949
|
||||
```
|
||||
|
||||
### Fetching the Sources From GitHub
|
||||
### Using Git Remotes
|
||||
|
||||
Since Hugo 0.48, Hugo uses the Go Modules support built into Go 1.11 to build. The easiest is to clone Hugo in a directory outside of `GOPATH`, as in the following example:
|
||||
Due to the way Go handles package imports, the best approach for working on a
|
||||
Hugo fork is to use Git Remotes. Here's a simple walk-through for getting
|
||||
started:
|
||||
|
||||
```bash
|
||||
mkdir $HOME/src
|
||||
cd $HOME/src
|
||||
git clone https://github.com/gohugoio/hugo.git
|
||||
cd hugo
|
||||
go install
|
||||
```
|
||||
1. Get the latest Hugo sources:
|
||||
|
||||
>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`.
|
||||
```
|
||||
go get -u -t github.com/spf13/hugo/...
|
||||
```
|
||||
|
||||
For some convenient build and test targets, you also will want to install Mage:
|
||||
1. Change to the Hugo source directory:
|
||||
|
||||
```bash
|
||||
go install github.com/magefile/mage
|
||||
```
|
||||
|
||||
Now, to make a change to Hugo's source:
|
||||
```
|
||||
cd $GOPATH/src/github.com/spf13/hugo
|
||||
```
|
||||
|
||||
1. Create a new branch for your changes (the branch name is arbitrary):
|
||||
|
||||
```bash
|
||||
```
|
||||
git checkout -b iss1234
|
||||
```
|
||||
|
||||
1. After making your changes, commit them to your new branch:
|
||||
|
||||
```bash
|
||||
```
|
||||
git commit -a -v
|
||||
```
|
||||
|
||||
1. Fork Hugo in GitHub.
|
||||
1. Fork Hugo in Github.
|
||||
|
||||
1. Add your fork as a new remote (the remote name, "fork" in this example, is arbitrary):
|
||||
|
||||
```bash
|
||||
git remote add fork git@github.com:USERNAME/hugo.git
|
||||
```
|
||||
git remote add fork git://github.com/USERNAME/hugo.git
|
||||
```
|
||||
|
||||
1. Push the changes to your new remote:
|
||||
|
||||
```bash
|
||||
```
|
||||
git push --set-upstream fork iss1234
|
||||
```
|
||||
|
||||
1. You're now ready to submit a PR based upon the new branch in your forked repository.
|
||||
|
||||
### Building Hugo with Your Changes
|
||||
|
||||
Hugo uses [mage](https://github.com/magefile/mage) to sync vendor dependencies, build Hugo, run the test suite and other things. You must run mage from the Hugo directory.
|
||||
### Build Hugo with Your Changes
|
||||
|
||||
```bash
|
||||
cd $HOME/go/src/github.com/gohugoio/hugo
|
||||
cd $GOPATH/src/github.com/spf13/hugo
|
||||
go build
|
||||
mv hugo /usr/local/bin/
|
||||
```
|
||||
|
||||
To build Hugo:
|
||||
### Add Compile Information to Hugo
|
||||
|
||||
```bash
|
||||
mage hugo
|
||||
```
|
||||
To add compile information to Hugo, replace the `go build` command with the following *(replace `/path/to/hugo` with the actual path)*:
|
||||
|
||||
To install hugo in `$HOME/go/bin`:
|
||||
go build -ldflags "-X /path/to/hugo/hugolib.CommitHash=`git rev-parse --short HEAD 2>/dev/null` -X github.com/spf13/hugo/hugolib.BuildDate=`date +%FT%T%z`"
|
||||
|
||||
```bash
|
||||
mage install
|
||||
```
|
||||
This will result in `hugo version` output that looks similar to:
|
||||
|
||||
To run the tests:
|
||||
Hugo Static Site Generator v0.13-DEV-8042E77 buildDate: 2014-12-25T03:25:57-07:00
|
||||
|
||||
```bash
|
||||
mage hugoRace
|
||||
mage -v check
|
||||
```
|
||||
Alternatively, just run `make` — all the “magic” above is already in the `Makefile`. :wink:
|
||||
|
||||
To list all available commands along with descriptions:
|
||||
|
||||
```bash
|
||||
mage -l
|
||||
```
|
||||
|
||||
**Note:** From Hugo 0.43 we have added a build tag, `extended` that adds **SCSS support**. This needs a C compiler installed to build. You can enable this when building by:
|
||||
|
||||
```bash
|
||||
HUGO_BUILD_TAGS=extended mage install
|
||||
````
|
||||
|
||||
Executable → Regular
+44
-40
@@ -1,45 +1,49 @@
|
||||
# GitHub: https://github.com/gohugoio
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
FROM golang:1.6.2
|
||||
MAINTAINER Sven Dowideit <SvenDowideit@home.org.au>
|
||||
|
||||
FROM golang:1.18-alpine AS build
|
||||
ENV GOPATH /go
|
||||
ENV USER root
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=extended .
|
||||
ARG HUGO_BUILD_TAGS
|
||||
# pre-install known dependencies before the source, so we don't redownload them whenever the source changes
|
||||
RUN go get github.com/stretchr/testify/assert \
|
||||
&& go get github.com/kyokomi/emoji \
|
||||
&& go get github.com/bep/inflect \
|
||||
&& go get github.com/BurntSushi/toml \
|
||||
&& go get github.com/PuerkitoBio/purell \
|
||||
&& go get github.com/PuerkitoBio/urlesc \
|
||||
&& go get github.com/dchest/cssmin \
|
||||
&& go get github.com/eknkc/amber \
|
||||
&& go get github.com/gorilla/websocket \
|
||||
&& go get github.com/kardianos/osext \
|
||||
&& go get github.com/miekg/mmark \
|
||||
&& go get github.com/mitchellh/mapstructure \
|
||||
&& go get github.com/russross/blackfriday \
|
||||
&& go get github.com/shurcooL/sanitized_anchor_name \
|
||||
&& go get github.com/spf13/afero \
|
||||
&& go get github.com/spf13/cast \
|
||||
&& go get github.com/spf13/jwalterweatherman \
|
||||
&& go get github.com/spf13/cobra \
|
||||
&& go get github.com/cpuguy83/go-md2man \
|
||||
&& go get github.com/inconshreveable/mousetrap \
|
||||
&& go get github.com/spf13/pflag \
|
||||
&& go get github.com/spf13/fsync \
|
||||
&& go get github.com/spf13/viper \
|
||||
&& go get github.com/kr/pretty \
|
||||
&& go get github.com/kr/text \
|
||||
&& go get github.com/magiconair/properties \
|
||||
&& go get golang.org/x/text/transform \
|
||||
&& go get golang.org/x/text/unicode/norm \
|
||||
&& go get github.com/yosssi/ace \
|
||||
&& go get github.com/spf13/nitro \
|
||||
&& go get github.com/fortytw2/leaktest \
|
||||
&& go get github.com/fsnotify/fsnotify
|
||||
|
||||
ARG CGO=1
|
||||
ENV CGO_ENABLED=${CGO}
|
||||
ENV GOOS=linux
|
||||
ENV GO111MODULE=on
|
||||
COPY . /go/src/github.com/spf13/hugo
|
||||
RUN go get -d -v github.com/spf13/hugo
|
||||
RUN go install github.com/spf13/hugo
|
||||
|
||||
WORKDIR /go/src/github.com/gohugoio/hugo
|
||||
WORKDIR /go/src/github.com/spf13/hugo
|
||||
RUN go get -d -v
|
||||
RUN go build -o hugo main.go
|
||||
RUN go test github.com/spf13/hugo/...
|
||||
|
||||
COPY . /go/src/github.com/gohugoio/hugo/
|
||||
|
||||
# gcc/g++ are required to build SASS libraries for extended version
|
||||
RUN apk update && \
|
||||
apk add --no-cache gcc g++ musl-dev git && \
|
||||
go install github.com/magefile/mage
|
||||
|
||||
RUN mage hugo && mage install
|
||||
|
||||
# ---
|
||||
|
||||
FROM alpine:3.12
|
||||
|
||||
COPY --from=build /go/bin/hugo /usr/bin/hugo
|
||||
|
||||
# libc6-compat & libstdc++ are required for extended SASS libraries
|
||||
# ca-certificates are required to fetch outside resources (like Twitter oEmbeds)
|
||||
RUN apk update && \
|
||||
apk add --no-cache ca-certificates libc6-compat libstdc++ git
|
||||
|
||||
VOLUME /site
|
||||
WORKDIR /site
|
||||
|
||||
# Expose port for live server
|
||||
EXPOSE 1313
|
||||
|
||||
ENTRYPOINT ["hugo"]
|
||||
CMD ["--help"]
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,83 @@
|
||||
|
||||
# Adds build information from git repo
|
||||
#
|
||||
# as suggested by tatsushid in
|
||||
# https://github.com/spf13/hugo/issues/540
|
||||
|
||||
COMMIT_HASH=`git rev-parse --short HEAD 2>/dev/null`
|
||||
BUILD_DATE=`date +%FT%T%z`
|
||||
LDFLAGS=-ldflags "-X github.com/spf13/hugo/hugolib.CommitHash=${COMMIT_HASH} -X github.com/spf13/hugo/hugolib.BuildDate=${BUILD_DATE}"
|
||||
PACKAGES = $(shell govendor list -no-status +local | sed 's/github.com.spf13.hugo/./')
|
||||
|
||||
all: gitinfo
|
||||
|
||||
install: install-gitinfo
|
||||
|
||||
help:
|
||||
echo ${COMMIT_HASH}
|
||||
echo ${BUILD_DATE}
|
||||
|
||||
gitinfo:
|
||||
go build ${LDFLAGS} -o hugo main.go
|
||||
|
||||
install-gitinfo:
|
||||
go install ${LDFLAGS} ./...
|
||||
|
||||
no-git-info:
|
||||
go build -o hugo main.go
|
||||
|
||||
docker:
|
||||
docker build -t hugo .
|
||||
docker rm -f hugo-build || true
|
||||
docker run --name hugo-build hugo ls /go/bin
|
||||
docker cp hugo-build:/go/bin/hugo .
|
||||
docker rm hugo-build
|
||||
|
||||
govendor:
|
||||
go get -u github.com/kardianos/govendor
|
||||
go install github.com/kardianos/govendor
|
||||
govendor sync github.com/spf13/hugo
|
||||
|
||||
check: fmt vet test test-race
|
||||
|
||||
cyclo:
|
||||
@for d in `govendor list -no-status +local | sed 's/github.com.spf13.hugo/./'` ; do \
|
||||
if [ "`gocyclo -over 20 $$d | tee /dev/stderr`" ]; then \
|
||||
echo "^ cyclomatic complexity exceeds 20, refactor the code!" && echo && exit 1; \
|
||||
fi \
|
||||
done
|
||||
|
||||
fmt:
|
||||
@for d in `govendor list -no-status +local | sed 's/github.com.spf13.hugo/./'` ; do \
|
||||
if [ "`gofmt -l $$d/*.go | tee /dev/stderr`" ]; then \
|
||||
echo "^ improperly formatted go files" && echo && exit 1; \
|
||||
fi \
|
||||
done
|
||||
|
||||
lint:
|
||||
@for d in `govendor list -no-status +local | sed 's/github.com.spf13.hugo/./'` ; do \
|
||||
if [ "`golint $$d | tee /dev/stderr`" ]; then \
|
||||
echo "^ golint errors!" && echo && exit 1; \
|
||||
fi \
|
||||
done
|
||||
|
||||
get:
|
||||
go get -v -t ./...
|
||||
|
||||
test:
|
||||
govendor test +local
|
||||
|
||||
test-race:
|
||||
govendor test -race +local
|
||||
|
||||
vet:
|
||||
@if [ "`govendor vet +local | tee /dev/stderr`" ]; then \
|
||||
echo "^ go vet errors!" && echo && exit 1; \
|
||||
fi
|
||||
|
||||
test-cover-html:
|
||||
echo "mode: count" > coverage-all.out
|
||||
$(foreach pkg,$(PACKAGES),\
|
||||
govendor test -coverprofile=coverage.out -covermode=count $(pkg);\
|
||||
tail -n +2 coverage.out >> coverage-all.out;)
|
||||
go tool cover -html=coverage-all.out
|
||||
@@ -1,92 +1,72 @@
|
||||
<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](https://github.com/bep), [spf13](https://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go][].
|
||||
A Fast and Flexible Static Site Generator built with love by [spf13](http://spf13.com/) and [friends](https://github.com/spf13/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/) |
|
||||
[Forum](https://discuss.gohugo.io) |
|
||||
[Developer Chat (no support)](https://gitter.im/spf13/hugo) |
|
||||
[Documentation](https://gohugo.io/overview/introduction/) |
|
||||
[Installation Guide](https://gohugo.io/overview/installing/) |
|
||||
[Contribution Guide](CONTRIBUTING.md) |
|
||||
[Twitter](https://twitter.com/gohugoio)
|
||||
[Twitter](http://twitter.com/spf13)
|
||||
|
||||
[](https://godoc.org/github.com/gohugoio/hugo)
|
||||
[](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
[](https://godoc.org/github.com/spf13/hugo)
|
||||
[](https://travis-ci.org/spf13/hugo)
|
||||
[](https://ci.appveyor.com/project/spf13/hugo/branch/master)
|
||||
[](https://gitter.im/spf13/hugo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://goreportcard.com/report/github.com/spf13/hugo)
|
||||
|
||||
## Overview
|
||||
|
||||
Hugo is a static HTML and CSS website generator written in [Go][].
|
||||
It is optimized for speed, ease of use, and configurability.
|
||||
It is optimized for speed, easy use and configurability.
|
||||
Hugo takes a directory with content and templates and renders them into a full HTML website.
|
||||
|
||||
Hugo relies on Markdown files with front matter for metadata, and you can run Hugo from any directory.
|
||||
Hugo relies on Markdown files with front matter for meta data.
|
||||
And you can run Hugo from any directory.
|
||||
This works well for shared hosts and other systems where you don’t have a privileged account.
|
||||
|
||||
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.
|
||||
|
||||
Hugo is designed to work well for any kind of website including blogs, tumbles, and docs.
|
||||
Hugo is designed to work well for any kind of website including blogs, tumbles and docs.
|
||||
|
||||
#### Supported Architectures
|
||||
|
||||
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.
|
||||
Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD and OS X (Darwin) for x64, i386 and ARM architectures.
|
||||
|
||||
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.
|
||||
Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including DragonFly BSD, OpenBSD, Plan 9 and Solaris.
|
||||
|
||||
**Complete documentation is available at [Hugo Documentation](https://gohugo.io/getting-started/).**
|
||||
**Complete documentation is available at [Hugo Documentation][].**
|
||||
|
||||
## Choose How to Install
|
||||
|
||||
If you want to use Hugo as your site generator, simply install the Hugo binaries.
|
||||
The Hugo binaries have no external dependencies.
|
||||
|
||||
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.
|
||||
To contribute to the Hugo source code or documentation, you should [fork the Hugo GitHub project](https://github.com/spf13/hugo#fork-destination-box) and clone it to your local machine.
|
||||
|
||||
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.
|
||||
|
||||
### Install Hugo as Your Site Generator (Binary Install)
|
||||
|
||||
Use the [installation instructions in the Hugo documentation](https://gohugo.io/getting-started/installing/).
|
||||
Use the [installation instructions in the Hugo documentation](https://gohugo.io/overview/installing/).
|
||||
|
||||
### Build and Install the Binaries from Source (Advanced Install)
|
||||
|
||||
#### Prerequisite Tools
|
||||
Add Hugo and its package dependencies to your go `src` directory.
|
||||
|
||||
* [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/)
|
||||
go get -v github.com/spf13/hugo
|
||||
|
||||
#### Fetch from GitHub
|
||||
Once the `get` completes, you should find your new `hugo` (or `hugo.exe`) executable sitting inside `$GOPATH/bin/`.
|
||||
|
||||
To fetch and build the source from GitHub:
|
||||
To update Hugo’s dependencies, use `go get` with the `-u` option.
|
||||
|
||||
```bash
|
||||
mkdir $HOME/src
|
||||
cd $HOME/src
|
||||
git clone https://github.com/gohugoio/hugo.git
|
||||
cd hugo
|
||||
go install
|
||||
```
|
||||
go get -u -v github.com/spf13/hugo
|
||||
|
||||
**If you are a Windows user, substitute the `$HOME` environment variable above with `%USERPROFILE%`.**
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## The Hugo Documentation
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:gohugoio/hugo.git
|
||||
```
|
||||
## Contributing to Hugo
|
||||
|
||||
**Note March 16th 2022:** We are currently very constrained on human resources to do code reviews, so we currently require any new Pull Requests to be limited to bug fixes closing an existing issue. Also, we have updated to Go 1.18, but we will currently not accept any generic rewrites, "interface{} to any" replacements and similar.
|
||||
|
||||
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
We welcome contributions to Hugo of any kind including documentation, themes,
|
||||
@@ -94,18 +74,20 @@ 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.
|
||||
The Hugo community and maintainers are very active and helpful, and the project benefits greatly from this activity.
|
||||
|
||||
[](https://waffle.io/spf13/hugo/metrics)
|
||||
|
||||
### 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.
|
||||
We have an active [discussion forum](http://discuss.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).
|
||||
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](http://discuss.gohugo.io).
|
||||
When reporting the issue, please provide the version of Hugo in use (`hugo version`).
|
||||
|
||||
### Submitting Patches
|
||||
@@ -113,136 +95,14 @@ When reporting the issue, please provide the version of Hugo in use (`hugo versi
|
||||
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,
|
||||
Because we want to create the best possible product for our users and the best contribution experience for our developers,
|
||||
we have a set of guidelines which ensure that all contributions are acceptable.
|
||||
The guidelines are not intended as a filter or barrier to participation.
|
||||
If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines.
|
||||
|
||||
For a complete guide to contributing code to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
[](https://github.com/igrigorik/ga-beacon)
|
||||
|
||||
[Go]: https://golang.org/
|
||||
[Hugo Documentation]: https://gohugo.io/overview/introduction/
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulder of many great open source libraries.
|
||||
|
||||
If you run `hugo env -v` you will get a complete and up to date list.
|
||||
|
||||
In Hugo 0.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="v0.10.0"
|
||||
github.com/armon/go-radix="v1.0.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="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/tmc="v0.5.1"
|
||||
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.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.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.0.0"
|
||||
github.com/golang/groupcache="v0.0.0-20210331224755-41bb18bfe9da"
|
||||
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/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.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/kr/pretty="v0.3.0"
|
||||
github.com/kr/text="v0.2.0"
|
||||
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/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.6.2"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
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/libsass="3.6.5"
|
||||
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.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"
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
## Security Policy
|
||||
|
||||
### Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days.
|
||||
|
||||
Also see [Hugo's Security Model](https://gohugo.io/about/security-model/).
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# allow user to override go executable by running as GOEXE=xxx make ...
|
||||
GOEXE="${GOEXE-go}"
|
||||
|
||||
# Convenience script to
|
||||
# - For a given branch
|
||||
@@ -28,10 +26,10 @@ BRANCH=$1
|
||||
PACKAGE=$2
|
||||
|
||||
git checkout $BRANCH
|
||||
"${GOEXE}" test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-$BRANCH.txt
|
||||
go test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-$BRANCH.txt
|
||||
|
||||
git checkout master
|
||||
"${GOEXE}" test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-master.txt
|
||||
go test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-master.txt
|
||||
|
||||
|
||||
benchcmp /tmp/bench-$PACKAGE-master.txt /tmp/bench-$PACKAGE-$BRANCH.txt
|
||||
benchcmp /tmp/bench-$PACKAGE-master.txt /tmp/bench-$PACKAGE-$BRANCH.txt
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# allow user to override go executable by running as GOEXE=xxx make ...
|
||||
GOEXE="${GOEXE-go}"
|
||||
|
||||
# Send in a regexp matching the benchmarks you want to run, i.e. './benchSite.sh "YAML"'.
|
||||
# Note the quotes, which will be needed for more complex expressions.
|
||||
# The above will run all variations, but only for front matter YAML.
|
||||
|
||||
echo "Running with BenchmarkSiteBuilding/${1}"
|
||||
|
||||
"${GOEXE}" test -run="NONE" -bench="BenchmarkSiteBuilding/${1}" -test.benchmem=true ./hugolib -memprofile mem.prof -count 3 -cpuprofile cpu.prof
|
||||
@@ -1 +0,0 @@
|
||||
gobench -package=./hugolib -bench="BenchmarkSiteNew/Deep_content_tree"
|
||||
@@ -1 +0,0 @@
|
||||
docker run --rm --mount type=bind,source="$(pwd)",target=/hugo -w /hugo -i -t bepsays/ci-goreleaser:1.11-2 /bin/bash
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
var bufferPool = &sync.Pool{
|
||||
New: func() any {
|
||||
New: func() interface{} {
|
||||
return &bytes.Buffer{}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -14,18 +14,14 @@
|
||||
package bufferpool
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestBufferPool(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
buff := GetBuffer()
|
||||
buff.WriteString("do be do be do")
|
||||
c.Assert(buff.String(), qt.Equals, "do be do be do")
|
||||
assert.Equal(t, "do be do be do", buff.String())
|
||||
PutBuffer(buff)
|
||||
|
||||
c.Assert(buff.Len(), qt.Equals, 0)
|
||||
assert.Equal(t, 0, buff.Len())
|
||||
}
|
||||
|
||||
Vendored
-384
@@ -1,384 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/BurntSushi/locker"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// ErrFatal can be used to signal an unrecoverable error.
|
||||
var ErrFatal = errors.New("fatal filecache error")
|
||||
|
||||
const (
|
||||
filecacheRootDirname = "filecache"
|
||||
)
|
||||
|
||||
// Cache caches a set of files in a directory. This is usually a file on
|
||||
// disk, but since this is backed by an Afero file system, it can be anything.
|
||||
type Cache struct {
|
||||
Fs afero.Fs
|
||||
|
||||
// Max age for items in this cache. Negative duration means forever,
|
||||
// 0 is effectively turning this cache off.
|
||||
maxAge time.Duration
|
||||
|
||||
// When set, we just remove this entire root directory on expiration.
|
||||
pruneAllRootDir string
|
||||
|
||||
nlocker *lockTracker
|
||||
}
|
||||
|
||||
type lockTracker struct {
|
||||
seenMu sync.RWMutex
|
||||
seen map[string]struct{}
|
||||
|
||||
*locker.Locker
|
||||
}
|
||||
|
||||
// Lock tracks the ids in use. We use this information to do garbage collection
|
||||
// after a Hugo build.
|
||||
func (l *lockTracker) Lock(id string) {
|
||||
l.seenMu.RLock()
|
||||
if _, seen := l.seen[id]; !seen {
|
||||
l.seenMu.RUnlock()
|
||||
l.seenMu.Lock()
|
||||
l.seen[id] = struct{}{}
|
||||
l.seenMu.Unlock()
|
||||
} else {
|
||||
l.seenMu.RUnlock()
|
||||
}
|
||||
|
||||
l.Locker.Lock(id)
|
||||
}
|
||||
|
||||
// ItemInfo contains info about a cached file.
|
||||
type ItemInfo struct {
|
||||
// This is the file's name relative to the cache's filesystem.
|
||||
Name string
|
||||
}
|
||||
|
||||
// NewCache creates a new file cache with the given filesystem and max age.
|
||||
func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
|
||||
return &Cache{
|
||||
Fs: fs,
|
||||
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
|
||||
maxAge: maxAge,
|
||||
pruneAllRootDir: pruneAllRootDir,
|
||||
}
|
||||
}
|
||||
|
||||
// lockedFile is a file with a lock that is released on Close.
|
||||
type lockedFile struct {
|
||||
afero.File
|
||||
unlock func()
|
||||
}
|
||||
|
||||
func (l *lockedFile) Close() error {
|
||||
defer l.unlock()
|
||||
return l.File.Close()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
id = cleanID(id)
|
||||
c.nlocker.Lock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
f, err := helpers.OpenFileForWriting(c.Fs, id)
|
||||
if err != nil {
|
||||
c.nlocker.Unlock(id)
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
return info, &lockedFile{
|
||||
File: f,
|
||||
unlock: func() { c.nlocker.Unlock(id) },
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadOrCreate tries to lookup the file in cache.
|
||||
// If found, it is passed to read and then closed.
|
||||
// If not found a new file is created and passed to create, which should close
|
||||
// it when done.
|
||||
func (c *Cache) ReadOrCreate(id string,
|
||||
read func(info ItemInfo, r io.ReadSeeker) error,
|
||||
create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info = ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
err = read(info, r)
|
||||
defer r.Close()
|
||||
if err == nil || err == ErrFatal {
|
||||
// See https://github.com/gohugoio/hugo/issues/6401
|
||||
// To recover from file corruption we handle read errors
|
||||
// as the cache item was not found.
|
||||
// Any file permission issue will also fail in the next step.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f, err := helpers.OpenFileForWriting(c.Fs, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = create(info, f)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will
|
||||
// be invoked and the result cached.
|
||||
// This method is protected by a named lock using the given id as identifier.
|
||||
func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (ItemInfo, io.ReadCloser, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
return info, r, nil
|
||||
}
|
||||
|
||||
var (
|
||||
r io.ReadCloser
|
||||
err error
|
||||
)
|
||||
|
||||
r, err = create()
|
||||
if err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return info, hugio.ToReadCloser(r), nil
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
return info,
|
||||
hugio.ToReadCloser(&buff),
|
||||
afero.WriteReader(c.Fs, id, io.TeeReader(r, &buff))
|
||||
}
|
||||
|
||||
// GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice.
|
||||
func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
defer r.Close()
|
||||
b, err := ioutil.ReadAll(r)
|
||||
return info, b, err
|
||||
}
|
||||
|
||||
var (
|
||||
b []byte
|
||||
err error
|
||||
)
|
||||
|
||||
b, err = create()
|
||||
if err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.maxAge == 0 {
|
||||
return info, b, nil
|
||||
}
|
||||
|
||||
if err := afero.WriteReader(c.Fs, id, bytes.NewReader(b)); err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
return info, b, nil
|
||||
}
|
||||
|
||||
// GetBytes gets the file content with the given id from the cache, nil if none found.
|
||||
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
defer r.Close()
|
||||
b, err := ioutil.ReadAll(r)
|
||||
return info, b, err
|
||||
}
|
||||
|
||||
return info, nil, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
r := c.getOrRemove(id)
|
||||
|
||||
return info, r, nil
|
||||
}
|
||||
|
||||
// getOrRemove gets the file with the given id. If it's expired, it will
|
||||
// be removed.
|
||||
func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.maxAge > 0 {
|
||||
fi, err := c.Fs.Stat(id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.isExpired(fi.ModTime()) {
|
||||
c.Fs.Remove(id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (c *Cache) isExpired(modTime time.Time) bool {
|
||||
if c.maxAge < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Note the use of time.Since here.
|
||||
// We cannot use Hugo's global Clock for this.
|
||||
return c.maxAge == 0 || time.Since(modTime) > c.maxAge
|
||||
}
|
||||
|
||||
// For testing
|
||||
func (c *Cache) getString(id string) string {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b, _ := ioutil.ReadAll(f)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Caches is a named set of caches.
|
||||
type Caches map[string]*Cache
|
||||
|
||||
// Get gets a named cache, nil if none found.
|
||||
func (f Caches) Get(name string) *Cache {
|
||||
return f[strings.ToLower(name)]
|
||||
}
|
||||
|
||||
// NewCaches creates a new set of file caches from the given
|
||||
// configuration.
|
||||
func NewCaches(p *helpers.PathSpec) (Caches, error) {
|
||||
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 {
|
||||
cfs = p.BaseFs.ResourcesCache
|
||||
} else {
|
||||
cfs = fs
|
||||
}
|
||||
|
||||
if cfs == nil {
|
||||
// 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.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 {
|
||||
pruneAllRootDir = "pkg"
|
||||
}
|
||||
|
||||
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func cleanID(name string) string {
|
||||
return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator)
|
||||
}
|
||||
Vendored
-248
@@ -1,248 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const (
|
||||
cachesConfigKey = "caches"
|
||||
|
||||
resourcesGenDir = ":resourceDir/_gen"
|
||||
cacheDirProject = ":cacheDir/:project"
|
||||
)
|
||||
|
||||
var defaultCacheConfig = Config{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: cacheDirProject,
|
||||
}
|
||||
|
||||
const (
|
||||
cacheKeyGetJSON = "getjson"
|
||||
cacheKeyGetCSV = "getcsv"
|
||||
cacheKeyImages = "images"
|
||||
cacheKeyAssets = "assets"
|
||||
cacheKeyModules = "modules"
|
||||
cacheKeyGetResource = "getresource"
|
||||
)
|
||||
|
||||
type Configs map[string]Config
|
||||
|
||||
func (c Configs) CacheDirModules() string {
|
||||
return c[cacheKeyModules].Dir
|
||||
}
|
||||
|
||||
var defaultCacheConfigs = Configs{
|
||||
cacheKeyModules: {
|
||||
MaxAge: -1,
|
||||
Dir: ":cacheDir/modules",
|
||||
},
|
||||
cacheKeyGetJSON: defaultCacheConfig,
|
||||
cacheKeyGetCSV: defaultCacheConfig,
|
||||
cacheKeyImages: {
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
},
|
||||
cacheKeyAssets: {
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
},
|
||||
cacheKeyGetResource: Config{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: cacheDirProject,
|
||||
},
|
||||
}
|
||||
|
||||
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.
|
||||
MaxAge time.Duration
|
||||
|
||||
// The directory where files are stored.
|
||||
Dir string
|
||||
|
||||
// Will resources/_gen will get its own composite filesystem that
|
||||
// also checks any theme.
|
||||
isResourceDir bool
|
||||
}
|
||||
|
||||
// GetJSONCache gets the file cache for getJSON.
|
||||
func (f Caches) GetJSONCache() *Cache {
|
||||
return f[cacheKeyGetJSON]
|
||||
}
|
||||
|
||||
// GetCSVCache gets the file cache for getCSV.
|
||||
func (f Caches) GetCSVCache() *Cache {
|
||||
return f[cacheKeyGetCSV]
|
||||
}
|
||||
|
||||
// ImageCache gets the file cache for processed images.
|
||||
func (f Caches) ImageCache() *Cache {
|
||||
return f[cacheKeyImages]
|
||||
}
|
||||
|
||||
// ModulesCache gets the file cache for Hugo Modules.
|
||||
func (f Caches) ModulesCache() *Cache {
|
||||
return f[cacheKeyModules]
|
||||
}
|
||||
|
||||
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
|
||||
func (f Caches) AssetsCache() *Cache {
|
||||
return f[cacheKeyAssets]
|
||||
}
|
||||
|
||||
// GetResourceCache gets the file cache for remote resources.
|
||||
func (f Caches) GetResourceCache() *Cache {
|
||||
return f[cacheKeyGetResource]
|
||||
}
|
||||
|
||||
func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
|
||||
c := make(Configs)
|
||||
valid := make(map[string]bool)
|
||||
// Add defaults
|
||||
for k, v := range defaultCacheConfigs {
|
||||
c[k] = v
|
||||
valid[k] = true
|
||||
}
|
||||
|
||||
m := cfg.GetStringMap(cachesConfigKey)
|
||||
|
||||
_, isOsFs := fs.(*afero.OsFs)
|
||||
|
||||
for k, v := range m {
|
||||
if _, ok := v.(maps.Params); !ok {
|
||||
continue
|
||||
}
|
||||
cc := defaultCacheConfig
|
||||
|
||||
dc := &mapstructure.DecoderConfig{
|
||||
Result: &cc,
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(dc)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
if err := decoder.Decode(v); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode filecache config: %w", err)
|
||||
}
|
||||
|
||||
if cc.Dir == "" {
|
||||
return c, errors.New("must provide cache Dir")
|
||||
}
|
||||
|
||||
name := strings.ToLower(k)
|
||||
if !valid[name] {
|
||||
return nil, fmt.Errorf("%q is not a valid cache name", name)
|
||||
}
|
||||
|
||||
c[name] = cc
|
||||
}
|
||||
|
||||
// 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, "/")
|
||||
parts := strings.Split(dir, "/")
|
||||
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
resolved, isResource, err := resolveDirPlaceholder(fs, cfg, part)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
if isResource {
|
||||
v.isResourceDir = true
|
||||
}
|
||||
parts[i] = resolved
|
||||
}
|
||||
}
|
||||
|
||||
dir = path.Join(parts...)
|
||||
if hadSlash {
|
||||
dir = "/" + dir
|
||||
}
|
||||
v.Dir = filepath.Clean(filepath.FromSlash(dir))
|
||||
|
||||
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.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.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.Dir = filepath.Join(v.Dir, filecacheRootDirname, k)
|
||||
} else {
|
||||
v.Dir = filepath.Join(v.Dir, k)
|
||||
}
|
||||
|
||||
if disabled {
|
||||
v.MaxAge = 0
|
||||
}
|
||||
|
||||
c[k] = v
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ...
|
||||
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":
|
||||
d, err := helpers.GetCacheDir(fs, cfg)
|
||||
return d, false, err
|
||||
case ":project":
|
||||
return filepath.Base(workingDir), false, nil
|
||||
}
|
||||
|
||||
return "", false, fmt.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder)
|
||||
}
|
||||
-198
@@ -1,198 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestDecodeConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archetypeDir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = "10m"
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = "11h"
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
[caches.getResource]
|
||||
dir = "/path/to/c4"
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
c.Assert(err, qt.IsNil)
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded, 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.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.Dir, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images"))
|
||||
|
||||
c4 := decoded["getresource"]
|
||||
c.Assert(c4.MaxAge, qt.Equals, time.Duration(-1))
|
||||
c.Assert(c4.Dir, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource"))
|
||||
}
|
||||
|
||||
func TestDecodeConfigIgnoreCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
ignoreCache = true
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = 1234
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = 3456
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
[caches.getResource]
|
||||
dir = "/path/to/c4"
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
c.Assert(err, qt.IsNil)
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(len(decoded), qt.Equals, 6)
|
||||
|
||||
for _, v := range decoded {
|
||||
c.Assert(v.MaxAge, qt.Equals, time.Duration(0))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeConfigDefault(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
cfg := newTestConfig()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
cfg.Set("resourceDir", "c:\\cache\\resources")
|
||||
cfg.Set("cacheDir", "c:\\cache\\thecache")
|
||||
|
||||
} else {
|
||||
cfg.Set("resourceDir", "/cache/resources")
|
||||
cfg.Set("cacheDir", "/cache/thecache")
|
||||
}
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
decoded, err := DecodeConfig(fs, cfg)
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(len(decoded), qt.Equals, 6)
|
||||
|
||||
imgConfig := decoded[cacheKeyImages]
|
||||
jsonConfig := decoded[cacheKeyGetJSON]
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
c.Assert(imgConfig.Dir, qt.Equals, filepath.FromSlash("_gen/images"))
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
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
-126
@@ -1,126 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Prune removes expired and unused items from this cache.
|
||||
// The last one requires a full build so the cache usage can be tracked.
|
||||
// Note that we operate directly on the filesystem here, so this is not
|
||||
// thread safe.
|
||||
func (c Caches) Prune() (int, error) {
|
||||
counter := 0
|
||||
for k, cache := range c {
|
||||
|
||||
count, err := cache.Prune(false)
|
||||
|
||||
counter += count
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return counter, fmt.Errorf("failed to prune cache %q: %w", k, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
// Prune removes expired and unused items from this cache.
|
||||
// If force is set, everything will be removed not considering expiry time.
|
||||
func (c *Cache) Prune(force bool) (int, error) {
|
||||
if c.pruneAllRootDir != "" {
|
||||
return c.pruneRootDir(force)
|
||||
}
|
||||
|
||||
counter := 0
|
||||
|
||||
err := afero.Walk(c.Fs, "", func(name string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
name = cleanID(name)
|
||||
|
||||
if info.IsDir() {
|
||||
f, err := c.Fs.Open(name)
|
||||
if err != nil {
|
||||
// This cache dir may not exist.
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.Readdirnames(1)
|
||||
if err == io.EOF {
|
||||
// Empty dir.
|
||||
err = c.Fs.Remove(name)
|
||||
}
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
shouldRemove := force || c.isExpired(info.ModTime())
|
||||
|
||||
if !shouldRemove && len(c.nlocker.seen) > 0 {
|
||||
// Remove it if it's not been touched/used in the last build.
|
||||
_, seen := c.nlocker.seen[name]
|
||||
shouldRemove = !seen
|
||||
}
|
||||
|
||||
if shouldRemove {
|
||||
err := c.Fs.Remove(name)
|
||||
if err == nil {
|
||||
counter++
|
||||
}
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return counter, err
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDir(force bool) (int, error) {
|
||||
info, err := c.Fs.Stat(c.pruneAllRootDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !force && !c.isExpired(info.ModTime()) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, c.pruneAllRootDir)
|
||||
}
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestPrune(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getjson]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/c"
|
||||
[caches.getcsv]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/d"
|
||||
[caches.assets]
|
||||
maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
[caches.images]
|
||||
maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
`
|
||||
|
||||
for _, name := range []string{cacheKeyGetCSV, cacheKeyGetJSON, cacheKeyAssets, cacheKeyImages} {
|
||||
msg := qt.Commentf("cache: %s", name)
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
caches, err := NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
cache := caches[name]
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
cache.GetOrCreateBytes(id, func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
})
|
||||
if i == 4 {
|
||||
// This will expire the first 5
|
||||
time.Sleep(201 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
count, err := caches.Prune()
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 5, msg)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.getString(id)
|
||||
if i < 5 {
|
||||
c.Assert(v, qt.Equals, "")
|
||||
} else {
|
||||
c.Assert(v, qt.Equals, "abc")
|
||||
}
|
||||
}
|
||||
|
||||
caches, err = NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
cache = caches[name]
|
||||
// Touch one and then prune.
|
||||
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
})
|
||||
|
||||
count, err = caches.Prune()
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 4)
|
||||
|
||||
// Now only the i5 should be left.
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.getString(id)
|
||||
if i != 5 {
|
||||
c.Assert(v, qt.Equals, "")
|
||||
} else {
|
||||
c.Assert(v, qt.Equals, "abc")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Vendored
-349
@@ -1,349 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/helpers"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestFileCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
tempWorkingDir, 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()
|
||||
|
||||
for _, test := range []struct {
|
||||
cacheDir string
|
||||
workingDir string
|
||||
}{
|
||||
// Run with same dirs twice to make sure that works.
|
||||
{tempCacheDir, tempWorkingDir},
|
||||
{tempCacheDir, tempWorkingDir},
|
||||
} {
|
||||
|
||||
configStr := `
|
||||
workingDir = "WORKING_DIR"
|
||||
resourceDir = "resources"
|
||||
cacheDir = "CACHEDIR"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = "10h"
|
||||
dir = ":cacheDir/c"
|
||||
|
||||
`
|
||||
|
||||
winPathSep := "\\\\"
|
||||
|
||||
replacer := strings.NewReplacer("CACHEDIR", test.cacheDir, "WORKING_DIR", test.workingDir)
|
||||
|
||||
configStr = replacer.Replace(configStr)
|
||||
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
|
||||
|
||||
p := newPathsSpec(t, osfs, configStr)
|
||||
|
||||
caches, err := 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")
|
||||
c.Assert(filename, qt.Equals, filepath.FromSlash("_gen/images/key"))
|
||||
|
||||
rf := func(s string) func() (io.ReadCloser, error) {
|
||||
return func() (io.ReadCloser, error) {
|
||||
return struct {
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
}{
|
||||
strings.NewReader(s),
|
||||
ioutil.NopCloser(nil),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
bf := func() ([]byte, error) {
|
||||
return []byte("bcd"), nil
|
||||
}
|
||||
|
||||
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, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, "abc")
|
||||
|
||||
info, b, err = ca.GetOrCreateBytes("b", bf)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(r, qt.Not(qt.IsNil))
|
||||
c.Assert(info.Name, qt.Equals, "b")
|
||||
c.Assert(string(b), qt.Equals, "bcd")
|
||||
|
||||
_, b, err = ca.GetOrCreateBytes("a", bf)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(b), qt.Equals, "abc")
|
||||
|
||||
_, r, err = ca.GetOrCreate("a", rf("bcd"))
|
||||
c.Assert(err, qt.IsNil)
|
||||
b, _ = ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, "abc")
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(caches.Get("getJSON"), qt.Not(qt.IsNil))
|
||||
|
||||
info, w, err := caches.ImageCache().WriteCloser("mykey")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(info.Name, qt.Equals, "mykey")
|
||||
io.WriteString(w, "Hugo is great!")
|
||||
w.Close()
|
||||
c.Assert(caches.ImageCache().getString("mykey"), qt.Equals, "Hugo is great!")
|
||||
|
||||
info, r, err := caches.ImageCache().Get("mykey")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(r, qt.Not(qt.IsNil))
|
||||
c.Assert(info.Name, qt.Equals, "mykey")
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
|
||||
info, b, err = caches.ImageCache().GetBytes("mykey")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(info.Name, qt.Equals, "mykey")
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileCacheConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getjson]
|
||||
maxAge = "1s"
|
||||
dir = "/cache/c"
|
||||
|
||||
`
|
||||
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
|
||||
caches, err := NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
const cacheName = "getjson"
|
||||
|
||||
filenameData := func(i int) (string, string) {
|
||||
data := fmt.Sprintf("data: %d", i)
|
||||
filename := fmt.Sprintf("file%d", i)
|
||||
return filename, data
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 20; j++ {
|
||||
ca := caches.Get(cacheName)
|
||||
c.Assert(ca, qt.Not(qt.IsNil))
|
||||
filename, data := filenameData(i)
|
||||
_, r, err := ca.GetOrCreate(filename, func() (io.ReadCloser, error) {
|
||||
return hugio.ToReadCloser(strings.NewReader(data)), nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, data)
|
||||
// Trigger some expiration.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}(i)
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
var result string
|
||||
|
||||
rf := func(failLevel int) func(info ItemInfo, r io.ReadSeeker) error {
|
||||
return func(info ItemInfo, r io.ReadSeeker) error {
|
||||
if failLevel > 0 {
|
||||
if failLevel > 1 {
|
||||
return ErrFatal
|
||||
}
|
||||
return errors.New("fail")
|
||||
}
|
||||
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
result = string(b)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cache := NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
|
||||
|
||||
const id = "a32"
|
||||
|
||||
_, err := cache.ReadOrCreate(id, rf(0), bf("v1"))
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(result, qt.Equals, "v1")
|
||||
_, err = cache.ReadOrCreate(id, rf(0), bf("v2"))
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(result, qt.Equals, "v1")
|
||||
_, err = cache.ReadOrCreate(id, rf(1), bf("v3"))
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(result, qt.Equals, "v3")
|
||||
_, err = cache.ReadOrCreate(id, rf(2), bf("v3"))
|
||||
c.Assert(err, qt.Equals, 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)
|
||||
initConfig(fs, cfg)
|
||||
config.SetBaseTestDefaults(cfg)
|
||||
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, nil)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
}
|
||||
Vendored
-78
@@ -1,78 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package namedmemcache provides a memory cache with a named lock. This is suitable
|
||||
// for situations where creating the cached resource can be time consuming or otherwise
|
||||
// resource hungry, or in situations where a "once only per key" is a requirement.
|
||||
package namedmemcache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/BurntSushi/locker"
|
||||
)
|
||||
|
||||
// Cache holds the cached values.
|
||||
type Cache struct {
|
||||
nlocker *locker.Locker
|
||||
cache map[string]cacheEntry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
value any
|
||||
err error
|
||||
}
|
||||
|
||||
// New creates a new cache.
|
||||
func New() *Cache {
|
||||
return &Cache{
|
||||
nlocker: locker.NewLocker(),
|
||||
cache: make(map[string]cacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// Clear clears the cache state.
|
||||
func (c *Cache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.cache = make(map[string]cacheEntry)
|
||||
c.nlocker = locker.NewLocker()
|
||||
}
|
||||
|
||||
// GetOrCreate tries to get the value with the given cache key, if not found
|
||||
// create will be called and cached.
|
||||
// This method is thread safe. It also guarantees that the create func for a given
|
||||
// key is invoked only once for this cache.
|
||||
func (c *Cache) GetOrCreate(key string, create func() (any, error)) (any, error) {
|
||||
c.mu.RLock()
|
||||
entry, found := c.cache[key]
|
||||
c.mu.RUnlock()
|
||||
|
||||
if found {
|
||||
return entry.value, entry.err
|
||||
}
|
||||
|
||||
c.nlocker.Lock(key)
|
||||
defer c.nlocker.Unlock(key)
|
||||
|
||||
// Create it.
|
||||
value, err := create()
|
||||
|
||||
c.mu.Lock()
|
||||
c.cache[key] = cacheEntry{value: value, err: err}
|
||||
c.mu.Unlock()
|
||||
|
||||
return value, err
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package namedmemcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestNamedCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
cache := New()
|
||||
|
||||
counter := 0
|
||||
create := func() (any, error) {
|
||||
counter++
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
v1, err := cache.GetOrCreate("a1", create)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v1, qt.Equals, 1)
|
||||
v2, err := cache.GetOrCreate("a2", create)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v2, qt.Equals, 2)
|
||||
}
|
||||
|
||||
cache.Clear()
|
||||
|
||||
v3, err := cache.GetOrCreate("a2", create)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v3, qt.Equals, 3)
|
||||
}
|
||||
|
||||
func TestNamedCacheConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cache := New()
|
||||
|
||||
create := func(i int) func() (any, error) {
|
||||
return func() (any, error) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
id := fmt.Sprintf("id%d", j)
|
||||
v, err := cache.GetOrCreate(id, create(j))
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, j)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package codegen contains helpers for code generation.
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Make room for insertions
|
||||
const weightWidth = 1000
|
||||
|
||||
// NewInspector creates a new Inspector given a source root.
|
||||
func NewInspector(root string) *Inspector {
|
||||
return &Inspector{ProjectRootDir: root}
|
||||
}
|
||||
|
||||
// Inspector provides methods to help code generation. It uses a combination
|
||||
// of reflection and source code AST to do the heavy lifting.
|
||||
type Inspector struct {
|
||||
ProjectRootDir string
|
||||
|
||||
init sync.Once
|
||||
|
||||
// Determines method order. Go's reflect sorts lexicographically, so
|
||||
// we must parse the source to preserve this order.
|
||||
methodWeight map[string]map[string]int
|
||||
}
|
||||
|
||||
// MethodsFromTypes create a method set from the include slice, excluding any
|
||||
// method in exclude.
|
||||
func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.Type) Methods {
|
||||
c.parseSource()
|
||||
|
||||
var methods Methods
|
||||
|
||||
excludes := make(map[string]bool)
|
||||
|
||||
if len(exclude) > 0 {
|
||||
for _, m := range c.MethodsFromTypes(exclude, nil) {
|
||||
excludes[m.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// There may be overlapping interfaces in types. Do a simple check for now.
|
||||
seen := make(map[string]bool)
|
||||
|
||||
nameAndPackage := func(t reflect.Type) (string, string) {
|
||||
var name, pkg string
|
||||
|
||||
isPointer := t.Kind() == reflect.Ptr
|
||||
|
||||
if isPointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
pkgPrefix := ""
|
||||
if pkgPath := t.PkgPath(); pkgPath != "" {
|
||||
pkgPath = strings.TrimSuffix(pkgPath, "/")
|
||||
_, shortPath := path.Split(pkgPath)
|
||||
pkgPrefix = shortPath + "."
|
||||
pkg = pkgPath
|
||||
}
|
||||
|
||||
name = t.Name()
|
||||
if name == "" {
|
||||
// interface{}
|
||||
name = t.String()
|
||||
}
|
||||
|
||||
if isPointer {
|
||||
pkgPrefix = "*" + pkgPrefix
|
||||
}
|
||||
|
||||
name = pkgPrefix + name
|
||||
|
||||
return name, pkg
|
||||
}
|
||||
|
||||
for _, t := range include {
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
|
||||
m := t.Method(i)
|
||||
if excludes[m.Name] || seen[m.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[m.Name] = true
|
||||
|
||||
if m.PkgPath != "" {
|
||||
// Not exported
|
||||
continue
|
||||
}
|
||||
|
||||
numIn := m.Type.NumIn()
|
||||
|
||||
ownerName, _ := nameAndPackage(t)
|
||||
|
||||
method := Method{Owner: t, OwnerName: ownerName, Name: m.Name}
|
||||
|
||||
for i := 0; i < numIn; i++ {
|
||||
in := m.Type.In(i)
|
||||
|
||||
name, pkg := nameAndPackage(in)
|
||||
|
||||
if pkg != "" {
|
||||
method.Imports = append(method.Imports, pkg)
|
||||
}
|
||||
|
||||
method.In = append(method.In, name)
|
||||
}
|
||||
|
||||
numOut := m.Type.NumOut()
|
||||
|
||||
if numOut > 0 {
|
||||
for i := 0; i < numOut; i++ {
|
||||
out := m.Type.Out(i)
|
||||
name, pkg := nameAndPackage(out)
|
||||
|
||||
if pkg != "" {
|
||||
method.Imports = append(method.Imports, pkg)
|
||||
}
|
||||
|
||||
method.Out = append(method.Out, name)
|
||||
}
|
||||
}
|
||||
|
||||
methods = append(methods, method)
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(methods, func(i, j int) bool {
|
||||
mi, mj := methods[i], methods[j]
|
||||
|
||||
wi := c.methodWeight[mi.OwnerName][mi.Name]
|
||||
wj := c.methodWeight[mj.OwnerName][mj.Name]
|
||||
|
||||
if wi == wj {
|
||||
return mi.Name < mj.Name
|
||||
}
|
||||
|
||||
return wi < wj
|
||||
})
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
func (c *Inspector) parseSource() {
|
||||
c.init.Do(func() {
|
||||
if !strings.Contains(c.ProjectRootDir, "hugo") {
|
||||
panic("dir must be set to the Hugo root")
|
||||
}
|
||||
|
||||
c.methodWeight = make(map[string]map[string]int)
|
||||
dirExcludes := regexp.MustCompile("docs|examples")
|
||||
fileExcludes := regexp.MustCompile("autogen")
|
||||
var filenames []string
|
||||
|
||||
filepath.Walk(c.ProjectRootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
if dirExcludes.MatchString(info.Name()) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(path, ".go") || fileExcludes.MatchString(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
filenames = append(filenames, path)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, filename := range filenames {
|
||||
|
||||
pkg := c.packageFromPath(filename)
|
||||
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
switch t := n.(type) {
|
||||
case *ast.TypeSpec:
|
||||
if t.Name.IsExported() {
|
||||
switch it := t.Type.(type) {
|
||||
case *ast.InterfaceType:
|
||||
iface := pkg + "." + t.Name.Name
|
||||
methodNames := collectMethodsRecursive(pkg, it.Methods.List)
|
||||
weights := make(map[string]int)
|
||||
weight := weightWidth
|
||||
for _, name := range methodNames {
|
||||
weights[name] = weight
|
||||
weight += weightWidth
|
||||
}
|
||||
c.methodWeight[iface] = weights
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Complement
|
||||
for _, v1 := range c.methodWeight {
|
||||
for k2, w := range v1 {
|
||||
if v, found := c.methodWeight[k2]; found {
|
||||
for k3, v3 := range v {
|
||||
v1[k3] = (v3 / weightWidth) + w
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Inspector) packageFromPath(p string) string {
|
||||
p = filepath.ToSlash(p)
|
||||
base := path.Base(p)
|
||||
if !strings.Contains(base, ".") {
|
||||
return base
|
||||
}
|
||||
return path.Base(strings.TrimSuffix(p, base))
|
||||
}
|
||||
|
||||
// Method holds enough information about it to recreate it.
|
||||
type Method struct {
|
||||
// The interface we extracted this method from.
|
||||
Owner reflect.Type
|
||||
|
||||
// String version of the above, on the form PACKAGE.NAME, e.g.
|
||||
// page.Page
|
||||
OwnerName string
|
||||
|
||||
// Method name.
|
||||
Name string
|
||||
|
||||
// Imports needed to satisfy the method signature.
|
||||
Imports []string
|
||||
|
||||
// Argument types, including any package prefix, e.g. string, int, interface{},
|
||||
// net.Url
|
||||
In []string
|
||||
|
||||
// Return types.
|
||||
Out []string
|
||||
}
|
||||
|
||||
// Declaration creates a method declaration (without any body) for the given receiver.
|
||||
func (m Method) Declaration(receiver string) string {
|
||||
return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStr())
|
||||
}
|
||||
|
||||
// DeclarationNamed creates a method declaration (without any body) for the given receiver
|
||||
// with named return values.
|
||||
func (m Method) DeclarationNamed(receiver string) string {
|
||||
return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStrNamed())
|
||||
}
|
||||
|
||||
// Delegate creates a delegate call string.
|
||||
func (m Method) Delegate(receiver, delegate string) string {
|
||||
ret := ""
|
||||
if len(m.Out) > 0 {
|
||||
ret = "return "
|
||||
}
|
||||
return fmt.Sprintf("%s%s.%s.%s%s", ret, receiverShort(receiver), delegate, m.Name, m.inOutStr())
|
||||
}
|
||||
|
||||
func (m Method) String() string {
|
||||
return m.Name + m.inStr() + " " + m.outStr() + "\n"
|
||||
}
|
||||
|
||||
func (m Method) inOutStr() string {
|
||||
if len(m.In) == 0 {
|
||||
return "()"
|
||||
}
|
||||
|
||||
args := make([]string, len(m.In))
|
||||
for i := 0; i < len(args); i++ {
|
||||
args[i] = fmt.Sprintf("arg%d", i)
|
||||
}
|
||||
return "(" + strings.Join(args, ", ") + ")"
|
||||
}
|
||||
|
||||
func (m Method) inStr() string {
|
||||
if len(m.In) == 0 {
|
||||
return "()"
|
||||
}
|
||||
|
||||
args := make([]string, len(m.In))
|
||||
for i := 0; i < len(args); i++ {
|
||||
args[i] = fmt.Sprintf("arg%d %s", i, m.In[i])
|
||||
}
|
||||
return "(" + strings.Join(args, ", ") + ")"
|
||||
}
|
||||
|
||||
func (m Method) outStr() string {
|
||||
if len(m.Out) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(m.Out) == 1 {
|
||||
return m.Out[0]
|
||||
}
|
||||
|
||||
return "(" + strings.Join(m.Out, ", ") + ")"
|
||||
}
|
||||
|
||||
func (m Method) outStrNamed() string {
|
||||
if len(m.Out) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
outs := make([]string, len(m.Out))
|
||||
for i := 0; i < len(outs); i++ {
|
||||
outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i])
|
||||
}
|
||||
|
||||
return "(" + strings.Join(outs, ", ") + ")"
|
||||
}
|
||||
|
||||
// Methods represents a list of methods for one or more interfaces.
|
||||
// The order matches the defined order in their source file(s).
|
||||
type Methods []Method
|
||||
|
||||
// Imports returns a sorted list of package imports needed to satisfy the
|
||||
// signatures of all methods.
|
||||
func (m Methods) Imports() []string {
|
||||
var pkgImports []string
|
||||
for _, method := range m {
|
||||
pkgImports = append(pkgImports, method.Imports...)
|
||||
}
|
||||
if len(pkgImports) > 0 {
|
||||
pkgImports = uniqueNonEmptyStrings(pkgImports)
|
||||
sort.Strings(pkgImports)
|
||||
}
|
||||
return pkgImports
|
||||
}
|
||||
|
||||
// ToMarshalJSON creates a MarshalJSON method for these methods. Any method name
|
||||
// matching any of the regexps in excludes will be ignored.
|
||||
func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (string, []string) {
|
||||
var sb strings.Builder
|
||||
|
||||
r := receiverShort(receiver)
|
||||
what := firstToUpper(trimAsterisk(receiver))
|
||||
pgkName := path.Base(pkgPath)
|
||||
|
||||
fmt.Fprintf(&sb, "func Marshal%sToJSON(%s %s) ([]byte, error) {\n", what, r, receiver)
|
||||
|
||||
var methods Methods
|
||||
excludeRes := make([]*regexp.Regexp, len(excludes))
|
||||
|
||||
for i, exclude := range excludes {
|
||||
excludeRes[i] = regexp.MustCompile(exclude)
|
||||
}
|
||||
|
||||
for _, method := range m {
|
||||
// Exclude methods with arguments and incompatible return values
|
||||
if len(method.In) > 0 || len(method.Out) == 0 || len(method.Out) > 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(method.Out) == 2 {
|
||||
if method.Out[1] != "error" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for _, re := range excludeRes {
|
||||
if re.MatchString(method.Name) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
methods = append(methods, method)
|
||||
}
|
||||
|
||||
for _, method := range methods {
|
||||
varn := varName(method.Name)
|
||||
if len(method.Out) == 1 {
|
||||
fmt.Fprintf(&sb, "\t%s := %s.%s()\n", varn, r, method.Name)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "\t%s, err := %s.%s()\n", varn, r, method.Name)
|
||||
fmt.Fprint(&sb, "\tif err != nil {\n\t\treturn nil, err\n\t}\n")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprint(&sb, "\n\ts := struct {\n")
|
||||
|
||||
for _, method := range methods {
|
||||
fmt.Fprintf(&sb, "\t\t%s %s\n", method.Name, typeName(method.Out[0], pgkName))
|
||||
}
|
||||
|
||||
fmt.Fprint(&sb, "\n\t}{\n")
|
||||
|
||||
for _, method := range methods {
|
||||
varn := varName(method.Name)
|
||||
fmt.Fprintf(&sb, "\t\t%s: %s,\n", method.Name, varn)
|
||||
}
|
||||
|
||||
fmt.Fprint(&sb, "\n\t}\n\n")
|
||||
fmt.Fprint(&sb, "\treturn json.Marshal(&s)\n}")
|
||||
|
||||
pkgImports := append(methods.Imports(), "encoding/json")
|
||||
|
||||
if pkgPath != "" {
|
||||
// Exclude self
|
||||
for i, pkgImp := range pkgImports {
|
||||
if pkgImp == pkgPath {
|
||||
pkgImports = append(pkgImports[:i], pkgImports[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), pkgImports
|
||||
}
|
||||
|
||||
func collectMethodsRecursive(pkg string, f []*ast.Field) []string {
|
||||
var methodNames []string
|
||||
for _, m := range f {
|
||||
if m.Names != nil {
|
||||
methodNames = append(methodNames, m.Names[0].Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if ident, ok := m.Type.(*ast.Ident); ok && ident.Obj != nil {
|
||||
// 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.
|
||||
name := packageName(m.Type)
|
||||
if !strings.Contains(name, ".") {
|
||||
// Assume current package
|
||||
name = pkg + "." + name
|
||||
}
|
||||
methodNames = append(methodNames, name)
|
||||
}
|
||||
}
|
||||
|
||||
return methodNames
|
||||
}
|
||||
|
||||
func firstToLower(name string) string {
|
||||
return strings.ToLower(name[:1]) + name[1:]
|
||||
}
|
||||
|
||||
func firstToUpper(name string) string {
|
||||
return strings.ToUpper(name[:1]) + name[1:]
|
||||
}
|
||||
|
||||
func packageName(e ast.Expr) string {
|
||||
switch tp := e.(type) {
|
||||
case *ast.Ident:
|
||||
return tp.Name
|
||||
case *ast.SelectorExpr:
|
||||
return fmt.Sprintf("%s.%s", packageName(tp.X), packageName(tp.Sel))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func receiverShort(receiver string) string {
|
||||
return strings.ToLower(trimAsterisk(receiver))[:1]
|
||||
}
|
||||
|
||||
func trimAsterisk(name string) string {
|
||||
return strings.TrimPrefix(name, "*")
|
||||
}
|
||||
|
||||
func typeName(name, pkg string) string {
|
||||
return strings.TrimPrefix(name, pkg+".")
|
||||
}
|
||||
|
||||
func uniqueNonEmptyStrings(s []string) []string {
|
||||
var unique []string
|
||||
set := map[string]any{}
|
||||
for _, val := range s {
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := set[val]; !ok {
|
||||
unique = append(unique, val)
|
||||
set[val] = val
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
func varName(name string) string {
|
||||
name = firstToLower(name)
|
||||
|
||||
// Adjust some reserved keywords, see https://golang.org/ref/spec#Keywords
|
||||
switch name {
|
||||
case "type":
|
||||
name = "typ"
|
||||
case "package":
|
||||
name = "pkg"
|
||||
// Not reserved, but syntax highlighters has it as a keyword.
|
||||
case "len":
|
||||
name = "length"
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package codegen
|
||||
|
||||
type IEmbed interface {
|
||||
MethodEmbed3(s string) string
|
||||
MethodEmbed1() string
|
||||
MethodEmbed2()
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
)
|
||||
|
||||
func TestMethods(t *testing.T) {
|
||||
var (
|
||||
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
|
||||
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
|
||||
zeroI = reflect.TypeOf((*I)(nil)).Elem()
|
||||
)
|
||||
|
||||
dir, _ := os.Getwd()
|
||||
insp := NewInspector(dir)
|
||||
|
||||
t.Run("MethodsFromTypes", func(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
methods := insp.MethodsFromTypes([]reflect.Type{zeroI}, nil)
|
||||
|
||||
methodsStr := fmt.Sprint(methods)
|
||||
|
||||
c.Assert(methodsStr, qt.Contains, "Method1(arg0 herrors.ErrorContext)")
|
||||
c.Assert(methodsStr, qt.Contains, "Method7() interface {}")
|
||||
c.Assert(methodsStr, qt.Contains, "Method0() string\n Method4() string")
|
||||
c.Assert(methodsStr, qt.Contains, "MethodEmbed3(arg0 string) string\n MethodEmbed1() string")
|
||||
|
||||
c.Assert(methods.Imports(), qt.Contains, "github.com/gohugoio/hugo/common/herrors")
|
||||
})
|
||||
|
||||
t.Run("EmbedOnly", func(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
methods := insp.MethodsFromTypes([]reflect.Type{zeroIEOnly}, nil)
|
||||
|
||||
methodsStr := fmt.Sprint(methods)
|
||||
|
||||
c.Assert(methodsStr, qt.Contains, "MethodEmbed3(arg0 string) string")
|
||||
})
|
||||
|
||||
t.Run("ToMarshalJSON", func(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m, pkg := insp.MethodsFromTypes(
|
||||
[]reflect.Type{zeroI},
|
||||
[]reflect.Type{zeroIE}).ToMarshalJSON("*page", "page")
|
||||
|
||||
c.Assert(m, qt.Contains, "method6 := p.Method6()")
|
||||
c.Assert(m, qt.Contains, "Method0: method0,")
|
||||
c.Assert(m, qt.Contains, "return json.Marshal(&s)")
|
||||
|
||||
c.Assert(pkg, qt.Contains, "github.com/gohugoio/hugo/common/herrors")
|
||||
c.Assert(pkg, qt.Contains, "encoding/json")
|
||||
|
||||
fmt.Println(pkg)
|
||||
})
|
||||
}
|
||||
|
||||
type I interface {
|
||||
IEmbed
|
||||
Method0() string
|
||||
Method4() string
|
||||
Method1(myerr herrors.ErrorContext)
|
||||
Method3(myint int, mystring string)
|
||||
Method5() (string, error)
|
||||
Method6() *net.IP
|
||||
Method7() any
|
||||
Method8() herrors.ErrorContext
|
||||
method2()
|
||||
method9() os.FileInfo
|
||||
}
|
||||
|
||||
type IEOnly interface {
|
||||
IEmbed
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var (
|
||||
benchmarkTimes int
|
||||
cpuProfileFile string
|
||||
memProfileFile string
|
||||
)
|
||||
|
||||
var benchmarkCmd = &cobra.Command{
|
||||
Use: "benchmark",
|
||||
Short: "Benchmark Hugo by building a site a number of times.",
|
||||
Long: `Hugo can build a site many times over and analyze the running process
|
||||
creating a benchmark.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
initHugoBuilderFlags(benchmarkCmd)
|
||||
initBenchmarkBuildingFlags(benchmarkCmd)
|
||||
|
||||
benchmarkCmd.Flags().StringVar(&cpuProfileFile, "cpuprofile", "", "path/filename for the CPU profile file")
|
||||
benchmarkCmd.Flags().StringVar(&memProfileFile, "memprofile", "", "path/filename for the memory profile file")
|
||||
benchmarkCmd.Flags().IntVarP(&benchmarkTimes, "count", "n", 13, "number of times to build the site")
|
||||
|
||||
benchmarkCmd.RunE = benchmark
|
||||
}
|
||||
|
||||
func benchmark(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
if err = InitializeConfig(benchmarkCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var memProf *os.File
|
||||
if memProfileFile != "" {
|
||||
memProf, err = os.Create(memProfileFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var cpuProf *os.File
|
||||
if cpuProfileFile != "" {
|
||||
cpuProf, err = os.Create(cpuProfileFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
memAllocated := memStats.TotalAlloc
|
||||
mallocs := memStats.Mallocs
|
||||
if cpuProf != nil {
|
||||
pprof.StartCPUProfile(cpuProf)
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
for i := 0; i < benchmarkTimes; i++ {
|
||||
if err = resetAndbuildSites(false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
totalTime := time.Since(t)
|
||||
|
||||
if memProf != nil {
|
||||
pprof.WriteHeapProfile(memProf)
|
||||
memProf.Close()
|
||||
}
|
||||
if cpuProf != nil {
|
||||
pprof.StopCPUProfile()
|
||||
cpuProf.Close()
|
||||
}
|
||||
|
||||
runtime.ReadMemStats(&memStats)
|
||||
totalMemAllocated := memStats.TotalAlloc - memAllocated
|
||||
totalMallocs := memStats.Mallocs - mallocs
|
||||
|
||||
jww.FEEDBACK.Println()
|
||||
jww.FEEDBACK.Printf("Average time per operation: %vms\n", int(1000*totalTime.Seconds()/float64(benchmarkTimes)))
|
||||
jww.FEEDBACK.Printf("Average memory allocated per operation: %vkB\n", totalMemAllocated/uint64(benchmarkTimes)/1024)
|
||||
jww.FEEDBACK.Printf("Average allocations per operation: %v\n", totalMallocs/uint64(benchmarkTimes))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var checkCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Contains some verification checks",
|
||||
}
|
||||
@@ -1,520 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
hconfig "github.com/gohugoio/hugo/config"
|
||||
|
||||
"golang.org/x/sync/semaphore"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/bep/clock"
|
||||
"github.com/bep/debounce"
|
||||
"github.com/bep/overlayfs"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
)
|
||||
|
||||
type commandeerHugoState struct {
|
||||
*deps.DepsCfg
|
||||
hugoSites *hugolib.HugoSites
|
||||
fsCreate sync.Once
|
||||
created chan struct{}
|
||||
}
|
||||
|
||||
type commandeer struct {
|
||||
*commandeerHugoState
|
||||
|
||||
logger loggers.Logger
|
||||
serverConfig *config.Server
|
||||
|
||||
buildLock func() (unlock func(), err error)
|
||||
|
||||
// Loading state
|
||||
mustHaveConfigFile bool
|
||||
failOnInitErr bool
|
||||
running bool
|
||||
|
||||
// Currently only set when in "fast render mode". But it seems to
|
||||
// be fast enough that we could maybe just add it for all server modes.
|
||||
changeDetector *fileChangeDetector
|
||||
|
||||
// We need to reuse these on server rebuilds.
|
||||
// These 2 will be different if --renderStaticToDisk is set.
|
||||
publishDirFs afero.Fs
|
||||
publishDirServerFs afero.Fs
|
||||
|
||||
h *hugoBuilderCommon
|
||||
ftch flagsToConfigHandler
|
||||
|
||||
visitedURLs *types.EvictingStringQueue
|
||||
|
||||
cfgInit func(c *commandeer) error
|
||||
|
||||
// We watch these for changes.
|
||||
configFiles []string
|
||||
|
||||
// Used in cases where we get flooded with events in server mode.
|
||||
debounce func(f func())
|
||||
|
||||
serverPorts []serverPortListener
|
||||
|
||||
languages langs.Languages
|
||||
doLiveReload bool
|
||||
renderStaticToDisk bool
|
||||
fastRenderMode bool
|
||||
showErrorInBrowser bool
|
||||
wasError bool
|
||||
|
||||
configured bool
|
||||
paused bool
|
||||
|
||||
fullRebuildSem *semaphore.Weighted
|
||||
|
||||
// Any error from the last build.
|
||||
buildErr error
|
||||
}
|
||||
|
||||
type serverPortListener struct {
|
||||
p int
|
||||
ln net.Listener
|
||||
}
|
||||
|
||||
func newCommandeerHugoState() *commandeerHugoState {
|
||||
return &commandeerHugoState{
|
||||
created: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *commandeerHugoState) hugo() *hugolib.HugoSites {
|
||||
<-c.created
|
||||
return c.hugoSites
|
||||
}
|
||||
|
||||
func (c *commandeerHugoState) hugoTry() *hugolib.HugoSites {
|
||||
select {
|
||||
case <-c.created:
|
||||
return c.hugoSites
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *commandeer) errCount() int {
|
||||
return int(c.logger.LogCounters().ErrorCounter.Count())
|
||||
}
|
||||
|
||||
func (c *commandeer) getErrorWithContext() any {
|
||||
errCount := c.errCount()
|
||||
|
||||
if errCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[string]any)
|
||||
|
||||
//xwm["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.logger.Errors())))
|
||||
m["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.logger.Errors())))
|
||||
m["Version"] = hugo.BuildVersionString()
|
||||
ferrors := herrors.UnwrapFileErrorsWithErrorContext(c.buildErr)
|
||||
m["Files"] = ferrors
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *commandeer) Set(key string, value any) {
|
||||
if c.configured {
|
||||
panic("commandeer cannot be changed")
|
||||
}
|
||||
c.Cfg.Set(key, value)
|
||||
}
|
||||
|
||||
func (c *commandeer) initFs(fs *hugofs.Fs) error {
|
||||
c.publishDirFs = fs.PublishDir
|
||||
c.publishDirServerFs = fs.PublishDirServer
|
||||
c.DepsCfg.Fs = fs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandeer) initClock(loc *time.Location) error {
|
||||
bt := c.Cfg.GetString("clock")
|
||||
if bt == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
t, err := cast.StringToDateInDefaultLocation(bt, loc)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse "clock" flag: %s`, err)
|
||||
}
|
||||
|
||||
htime.Clock = clock.Start(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func newCommandeer(mustHaveConfigFile, failOnInitErr, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, cfgInit func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||
var rebuildDebouncer func(f func())
|
||||
if running {
|
||||
// The time value used is tested with mass content replacements in a fairly big Hugo site.
|
||||
// It is better to wait for some seconds in those cases rather than get flooded
|
||||
// with rebuilds.
|
||||
rebuildDebouncer = debounce.New(4 * time.Second)
|
||||
}
|
||||
|
||||
out := ioutil.Discard
|
||||
if !h.quiet {
|
||||
out = os.Stdout
|
||||
}
|
||||
|
||||
c := &commandeer{
|
||||
h: h,
|
||||
ftch: f,
|
||||
commandeerHugoState: newCommandeerHugoState(),
|
||||
cfgInit: cfgInit,
|
||||
visitedURLs: types.NewEvictingStringQueue(10),
|
||||
debounce: rebuildDebouncer,
|
||||
fullRebuildSem: semaphore.NewWeighted(1),
|
||||
|
||||
// Init state
|
||||
mustHaveConfigFile: mustHaveConfigFile,
|
||||
failOnInitErr: failOnInitErr,
|
||||
running: running,
|
||||
|
||||
// This will be replaced later, but we need something to log to before the configuration is read.
|
||||
logger: loggers.NewLogger(jww.LevelWarn, jww.LevelError, out, ioutil.Discard, running),
|
||||
}
|
||||
|
||||
return c, c.loadConfig()
|
||||
}
|
||||
|
||||
type fileChangeDetector struct {
|
||||
sync.Mutex
|
||||
current map[string]string
|
||||
prev map[string]string
|
||||
|
||||
irrelevantRe *regexp.Regexp
|
||||
}
|
||||
|
||||
func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.current[name] = md5sum
|
||||
}
|
||||
|
||||
func (f *fileChangeDetector) changed() []string {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
var c []string
|
||||
for k, v := range f.current {
|
||||
vv, found := f.prev[k]
|
||||
if !found || v != vv {
|
||||
c = append(c, k)
|
||||
}
|
||||
}
|
||||
|
||||
return f.filterIrrelevant(c)
|
||||
}
|
||||
|
||||
func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
|
||||
var filtered []string
|
||||
for _, v := range in {
|
||||
if !f.irrelevantRe.MatchString(v) {
|
||||
filtered = append(filtered, v)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (f *fileChangeDetector) PrepareNew() {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
|
||||
if f.current == nil {
|
||||
f.current = make(map[string]string)
|
||||
f.prev = make(map[string]string)
|
||||
return
|
||||
}
|
||||
|
||||
f.prev = make(map[string]string)
|
||||
for k, v := range f.current {
|
||||
f.prev[k] = v
|
||||
}
|
||||
f.current = make(map[string]string)
|
||||
}
|
||||
|
||||
func (c *commandeer) loadConfig() error {
|
||||
if c.DepsCfg == nil {
|
||||
c.DepsCfg = &deps.DepsCfg{}
|
||||
}
|
||||
|
||||
if c.logger != nil {
|
||||
// Truncate the error log if this is a reload.
|
||||
c.logger.Reset()
|
||||
}
|
||||
|
||||
cfg := c.DepsCfg
|
||||
c.configured = false
|
||||
cfg.Running = c.running
|
||||
|
||||
var dir string
|
||||
if c.h.source != "" {
|
||||
dir, _ = filepath.Abs(c.h.source)
|
||||
} else {
|
||||
dir, _ = os.Getwd()
|
||||
}
|
||||
|
||||
var sourceFs afero.Fs = hugofs.Os
|
||||
if c.DepsCfg.Fs != nil {
|
||||
sourceFs = c.DepsCfg.Fs.Source
|
||||
}
|
||||
|
||||
environment := c.h.getEnvironment(c.running)
|
||||
|
||||
doWithConfig := func(cfg config.Provider) error {
|
||||
if c.ftch != nil {
|
||||
c.ftch.flagsToConfig(cfg)
|
||||
}
|
||||
|
||||
cfg.Set("workingDir", dir)
|
||||
cfg.Set("environment", environment)
|
||||
return nil
|
||||
}
|
||||
|
||||
cfgSetAndInit := func(cfg config.Provider) error {
|
||||
c.Cfg = cfg
|
||||
if c.cfgInit == nil {
|
||||
return nil
|
||||
}
|
||||
err := c.cfgInit(c)
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := c.h.source
|
||||
if configPath == "" {
|
||||
configPath = dir
|
||||
}
|
||||
config, configFiles, err := hugolib.LoadConfig(
|
||||
hugolib.ConfigSourceDescriptor{
|
||||
Fs: sourceFs,
|
||||
Logger: c.logger,
|
||||
Path: configPath,
|
||||
WorkingDir: dir,
|
||||
Filename: c.h.cfgFile,
|
||||
AbsConfigDir: c.h.getConfigDir(dir),
|
||||
Environment: environment,
|
||||
},
|
||||
cfgSetAndInit,
|
||||
doWithConfig)
|
||||
|
||||
if err != nil {
|
||||
// We should improve the error handling here,
|
||||
// but with hugo mod init and similar there is a chicken and egg situation
|
||||
// with modules already configured in config.toml, so ignore those errors.
|
||||
if c.mustHaveConfigFile || (c.failOnInitErr && !moduleNotFoundRe.MatchString(err.Error())) {
|
||||
return err
|
||||
} else {
|
||||
// Just make it a warning.
|
||||
c.logger.Warnln(err)
|
||||
}
|
||||
} else if c.mustHaveConfigFile && len(configFiles) == 0 {
|
||||
return hugolib.ErrNoConfigFile
|
||||
}
|
||||
|
||||
c.configFiles = configFiles
|
||||
|
||||
var ok bool
|
||||
loc := time.Local
|
||||
c.languages, ok = c.Cfg.Get("languagesSorted").(langs.Languages)
|
||||
if ok {
|
||||
loc = langs.GetLocation(c.languages[0])
|
||||
}
|
||||
|
||||
err = c.initClock(loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set some commonly used flags
|
||||
c.doLiveReload = c.running && !c.Cfg.GetBool("disableLiveReload")
|
||||
c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
|
||||
c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
|
||||
|
||||
// This is potentially double work, but we need to do this one more time now
|
||||
// that all the languages have been configured.
|
||||
if c.cfgInit != nil {
|
||||
if err := c.cfgInit(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logger, err := c.createLogger(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Logger = logger
|
||||
c.logger = logger
|
||||
c.serverConfig, err = hconfig.DecodeServer(cfg.Cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createMemFs := config.GetBool("renderToMemory")
|
||||
c.renderStaticToDisk = config.GetBool("renderStaticToDisk")
|
||||
|
||||
if createMemFs {
|
||||
// Rendering to memoryFS, publish to Root regardless of publishDir.
|
||||
config.Set("publishDir", "/")
|
||||
config.Set("publishDirStatic", "/")
|
||||
} else if c.renderStaticToDisk {
|
||||
// Hybrid, render dynamic content to Root.
|
||||
config.Set("publishDirStatic", config.Get("publishDir"))
|
||||
config.Set("publishDir", "/")
|
||||
|
||||
}
|
||||
|
||||
c.fsCreate.Do(func() {
|
||||
// Assume both source and destination are using same filesystem.
|
||||
fs := hugofs.NewFromSourceAndDestination(sourceFs, sourceFs, config)
|
||||
|
||||
if c.publishDirFs != nil {
|
||||
// Need to reuse the destination on server rebuilds.
|
||||
fs.PublishDir = c.publishDirFs
|
||||
fs.PublishDirServer = c.publishDirServerFs
|
||||
} else {
|
||||
if c.renderStaticToDisk {
|
||||
publishDirStatic := config.GetString("publishDirStatic")
|
||||
workingDir := config.GetString("workingDir")
|
||||
absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
|
||||
|
||||
fs = hugofs.NewFromSourceAndDestination(sourceFs, afero.NewMemMapFs(), config)
|
||||
// Writes the dynamic output to memory,
|
||||
// while serve others directly from /public on disk.
|
||||
dynamicFs := fs.PublishDir
|
||||
staticFs := afero.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic)
|
||||
|
||||
// Serve from both the static and dynamic fs,
|
||||
// the first will take priority.
|
||||
// THis is a read-only filesystem,
|
||||
// we do all the writes to
|
||||
// fs.Destination and fs.DestinationStatic.
|
||||
fs.PublishDirServer = overlayfs.New(
|
||||
overlayfs.Options{
|
||||
Fss: []afero.Fs{
|
||||
dynamicFs,
|
||||
staticFs,
|
||||
},
|
||||
},
|
||||
)
|
||||
fs.PublishDirStatic = staticFs
|
||||
} else if createMemFs {
|
||||
// Hugo writes the output to memory instead of the disk.
|
||||
fs = hugofs.NewFromSourceAndDestination(sourceFs, afero.NewMemMapFs(), config)
|
||||
}
|
||||
}
|
||||
|
||||
if c.fastRenderMode {
|
||||
// For now, fast render mode only. It should, however, be fast enough
|
||||
// for the full variant, too.
|
||||
changeDetector := &fileChangeDetector{
|
||||
// We use this detector to decide to do a Hot reload of a single path or not.
|
||||
// We need to filter out source maps and possibly some other to be able
|
||||
// to make that decision.
|
||||
irrelevantRe: regexp.MustCompile(`\.map$`),
|
||||
}
|
||||
|
||||
changeDetector.PrepareNew()
|
||||
fs.PublishDir = hugofs.NewHashingFs(fs.PublishDir, changeDetector)
|
||||
fs.PublishDirStatic = hugofs.NewHashingFs(fs.PublishDirStatic, changeDetector)
|
||||
c.changeDetector = changeDetector
|
||||
}
|
||||
|
||||
if c.Cfg.GetBool("logPathWarnings") {
|
||||
// Note that we only care about the "dynamic creates" here,
|
||||
// so skip the static fs.
|
||||
fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir)
|
||||
}
|
||||
|
||||
// To debug hard-to-find path issues.
|
||||
// fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
|
||||
|
||||
err = c.initFs(fs)
|
||||
if err != nil {
|
||||
close(c.created)
|
||||
return
|
||||
}
|
||||
|
||||
var h *hugolib.HugoSites
|
||||
|
||||
var createErr error
|
||||
h, createErr = hugolib.NewHugoSites(*c.DepsCfg)
|
||||
if h == nil || c.failOnInitErr {
|
||||
err = createErr
|
||||
}
|
||||
|
||||
c.hugoSites = h
|
||||
// TODO(bep) improve.
|
||||
if c.buildLock == nil && h != nil {
|
||||
c.buildLock = h.LockBuild
|
||||
}
|
||||
close(c.created)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cacheDir, err := helpers.GetCacheDir(sourceFs, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Set("cacheDir", cacheDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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)
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.Print the version number of Hug
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return 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
|
||||
}
|
||||
|
||||
allSettings := cfg.Cfg.Get("").(maps.Params)
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
type modMounts struct {
|
||||
verbose bool
|
||||
m modules.Module
|
||||
}
|
||||
|
||||
type modMount struct {
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
Lang string `json:"lang,omitempty"`
|
||||
}
|
||||
|
||||
func (m *modMounts) MarshalJSON() ([]byte, error) {
|
||||
var mounts []modMount
|
||||
|
||||
for _, mount := range m.m.Mounts() {
|
||||
mounts = append(mounts, modMount{
|
||||
Source: mount.Source,
|
||||
Target: mount.Target,
|
||||
Lang: mount.Lang,
|
||||
})
|
||||
}
|
||||
|
||||
var ownerPath string
|
||||
if m.m.Owner() != nil {
|
||||
ownerPath = m.m.Owner().Path()
|
||||
}
|
||||
|
||||
if m.verbose {
|
||||
config := m.m.Config()
|
||||
return json.Marshal(&struct {
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Time time.Time `json:"time"`
|
||||
Owner string `json:"owner"`
|
||||
Dir string `json:"dir"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
HugoVersion modules.HugoVersion `json:"hugoVersion"`
|
||||
|
||||
Mounts []modMount `json:"mounts"`
|
||||
}{
|
||||
Path: m.m.Path(),
|
||||
Version: m.m.Version(),
|
||||
Time: m.m.Time(),
|
||||
Owner: ownerPath,
|
||||
Dir: m.m.Dir(),
|
||||
Meta: config.Params,
|
||||
HugoVersion: config.HugoVersion,
|
||||
Mounts: mounts,
|
||||
})
|
||||
}
|
||||
|
||||
return json.Marshal(&struct {
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Time time.Time `json:"time"`
|
||||
Owner string `json:"owner"`
|
||||
Dir string `json:"dir"`
|
||||
Mounts []modMount `json:"mounts"`
|
||||
}{
|
||||
Path: m.m.Path(),
|
||||
Version: m.m.Version(),
|
||||
Time: m.m.Time(),
|
||||
Owner: ownerPath,
|
||||
Dir: m.m.Dir(),
|
||||
Mounts: mounts,
|
||||
})
|
||||
|
||||
}
|
||||
+115
-161
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 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,194 +14,148 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/parser/pageparser"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/spf13/hugo/hugolib"
|
||||
"github.com/spf13/hugo/parser"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var _ cmder = (*convertCmd)(nil)
|
||||
var outputDir string
|
||||
var unsafe bool
|
||||
|
||||
type convertCmd struct {
|
||||
outputDir string
|
||||
unsafe bool
|
||||
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
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.
|
||||
var convertCmd = &cobra.Command{
|
||||
Use: "convert",
|
||||
Short: "Convert your content to different formats",
|
||||
Long: `Convert your content (e.g. front matter) to different formats.
|
||||
|
||||
See convert's subcommands toJSON, toTOML and toYAML for more information.`,
|
||||
RunE: nil,
|
||||
}
|
||||
|
||||
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
|
||||
RunE: nil,
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
var toJSONCmd = &cobra.Command{
|
||||
Use: "toJSON",
|
||||
Short: "Convert front matter to JSON",
|
||||
Long: `toJSON converts all front matter in the content directory
|
||||
to use JSON for the front matter.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return convertContents(rune([]byte(parser.JSONLead)[0]))
|
||||
},
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, false, false, &cc.hugoBuilderCommon, cc, nil)
|
||||
if err != nil {
|
||||
var toTOMLCmd = &cobra.Command{
|
||||
Use: "toTOML",
|
||||
Short: "Convert front matter to TOML",
|
||||
Long: `toTOML converts all front matter in the content directory
|
||||
to use TOML for the front matter.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return convertContents(rune([]byte(parser.TOMLLead)[0]))
|
||||
},
|
||||
}
|
||||
|
||||
var toYAMLCmd = &cobra.Command{
|
||||
Use: "toYAML",
|
||||
Short: "Convert front matter to YAML",
|
||||
Long: `toYAML converts all front matter in the content directory
|
||||
to use YAML for the front matter.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return convertContents(rune([]byte(parser.YAMLLead)[0]))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
convertCmd.AddCommand(toJSONCmd)
|
||||
convertCmd.AddCommand(toTOMLCmd)
|
||||
convertCmd.AddCommand(toYAMLCmd)
|
||||
convertCmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "filesystem path to write files to")
|
||||
convertCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from")
|
||||
convertCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "enable less safe operations, please backup first")
|
||||
convertCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
}
|
||||
|
||||
func convertContents(mark rune) (err error) {
|
||||
if err := InitializeConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Cfg.Set("buildDrafts", true)
|
||||
h, err := hugolib.NewHugoSitesFromConfiguration()
|
||||
|
||||
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 (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 := cc.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if p.File().IsZero() {
|
||||
// No content file.
|
||||
return nil
|
||||
}
|
||||
|
||||
errMsg := fmt.Errorf("Error processing file %q", p.File().Path())
|
||||
|
||||
site.Log.Infoln("Attempting to convert", p.File().Filename())
|
||||
|
||||
f := p.File()
|
||||
file, err := f.FileInfo().Meta().Open()
|
||||
if err != nil {
|
||||
site.Log.Errorln(errMsg)
|
||||
file.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
pf, err := pageparser.ParseFrontMatterAndContent(file)
|
||||
if err != nil {
|
||||
site.Log.Errorln(errMsg)
|
||||
file.Close()
|
||||
if err = site.Initialise(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file.Close()
|
||||
if site.Source == nil {
|
||||
panic("site.Source not set")
|
||||
}
|
||||
if len(site.Source.Files()) < 1 {
|
||||
return errors.New("No source files found")
|
||||
}
|
||||
|
||||
// better handling of dates in formats that don't have support for them
|
||||
if pf.FrontMatterFormat == metadecoders.JSON || pf.FrontMatterFormat == metadecoders.YAML || pf.FrontMatterFormat == metadecoders.TOML {
|
||||
for k, v := range pf.FrontMatter {
|
||||
switch vv := v.(type) {
|
||||
case time.Time:
|
||||
pf.FrontMatter[k] = vv.Format(time.RFC3339)
|
||||
contentDir := helpers.AbsPathify(viper.GetString("contentDir"))
|
||||
jww.FEEDBACK.Println("processing", len(site.Source.Files()), "content files")
|
||||
for _, file := range site.Source.Files() {
|
||||
jww.INFO.Println("Attempting to convert", file.LogicalName())
|
||||
page, err := hugolib.NewPage(file.LogicalName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
psr, err := parser.ReadFrom(file.Contents)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Error processing file:", file.Path())
|
||||
return err
|
||||
}
|
||||
metadata, err := psr.Metadata()
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Error processing file:", file.Path())
|
||||
return err
|
||||
}
|
||||
|
||||
// better handling of dates in formats that don't have support for them
|
||||
if mark == parser.FormatToLeadRune("json") || mark == parser.FormatToLeadRune("yaml") || mark == parser.FormatToLeadRune("toml") {
|
||||
newmetadata := cast.ToStringMap(metadata)
|
||||
for k, v := range newmetadata {
|
||||
switch vv := v.(type) {
|
||||
case time.Time:
|
||||
newmetadata[k] = vv.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
metadata = newmetadata
|
||||
}
|
||||
|
||||
page.SetDir(filepath.Join(contentDir, file.Dir()))
|
||||
page.SetSourceContent(psr.Content())
|
||||
if err = page.SetSourceMetaData(metadata, mark); err != nil {
|
||||
jww.ERROR.Printf("Failed to set source metadata for file %q: %s. For more info see For more info see https://github.com/spf13/hugo/issues/2458", page.FullFilePath(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if outputDir != "" {
|
||||
if err = page.SaveSourceAs(filepath.Join(outputDir, page.FullFilePath())); err != nil {
|
||||
return fmt.Errorf("Failed to save file %q: %s", page.FullFilePath(), err)
|
||||
}
|
||||
} else {
|
||||
if unsafe {
|
||||
if err = page.SaveSource(); err != nil {
|
||||
return fmt.Errorf("Failed to save file %q: %s", page.FullFilePath(), err)
|
||||
}
|
||||
} else {
|
||||
jww.FEEDBACK.Println("Unsafe operation not allowed, use --unsafe or set a different output path")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newContent bytes.Buffer
|
||||
err = parser.InterfaceToFrontMatter(pf.FrontMatter, targetFormat, &newContent)
|
||||
if err != nil {
|
||||
site.Log.Errorln(errMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
newContent.Write(pf.Content)
|
||||
|
||||
newFilename := p.File().Filename()
|
||||
|
||||
if cc.outputDir != "" {
|
||||
contentDir := strings.TrimSuffix(newFilename, p.File().Path())
|
||||
contentDir = filepath.Base(contentDir)
|
||||
|
||||
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 nil
|
||||
}
|
||||
|
||||
type parsedFile struct {
|
||||
frontMatterFormat metadecoders.Format
|
||||
frontMatterSource []byte
|
||||
frontMatter map[string]any
|
||||
|
||||
// Everything after Front Matter
|
||||
content []byte
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gohugoio/hugo/deploy"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var _ cmder = (*deployCmd)(nil)
|
||||
|
||||
// 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.
|
||||
`,
|
||||
|
||||
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(comm.Cfg, comm.hugo().PathSpec.PublishFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
+11
-36
@@ -16,45 +16,20 @@ package commands
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var _ cmder = (*envCmd)(nil)
|
||||
|
||||
type envCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
}),
|
||||
}
|
||||
var envCmd = &cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Print Hugo version and environment info",
|
||||
Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
printHugoVersion()
|
||||
jww.FEEDBACK.Printf("GOOS=%q\n", runtime.GOOS)
|
||||
jww.FEEDBACK.Printf("GOARCH=%q\n", runtime.GOARCH)
|
||||
jww.FEEDBACK.Printf("GOVERSION=%q\n", runtime.Version())
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
+3
-20
@@ -17,24 +17,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var _ cmder = (*genCmd)(nil)
|
||||
|
||||
type genCmd struct {
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newGenCmd() *genCmd {
|
||||
cc := &genCmd{}
|
||||
cc.baseCmd = newBaseCmd(&cobra.Command{
|
||||
Use: "gen",
|
||||
Short: "A collection of several useful generators.",
|
||||
})
|
||||
|
||||
cc.cmd.AddCommand(
|
||||
newGenDocCmd().getCommand(),
|
||||
newGenManCmd().getCommand(),
|
||||
createGenDocsHelper().getCommand(),
|
||||
createGenChromaStyles().getCommand())
|
||||
|
||||
return cc
|
||||
var genCmd = &cobra.Command{
|
||||
Use: "gen",
|
||||
Short: "A collection of several useful generators.",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var autocompleteTarget string
|
||||
|
||||
// bash for now (zsh and others will come)
|
||||
var autocompleteType string
|
||||
|
||||
var genautocompleteCmd = &cobra.Command{
|
||||
Use: "autocomplete",
|
||||
Short: "Generate shell autocompletion script for Hugo",
|
||||
Long: `Generates a shell autocompletion script for Hugo.
|
||||
|
||||
NOTE: The current version supports Bash only.
|
||||
This should work for *nix systems with Bash installed.
|
||||
|
||||
By default, the file is written directly to /etc/bash_completion.d
|
||||
for convenience, and the command may need superuser rights, e.g.:
|
||||
|
||||
$ sudo hugo gen autocomplete
|
||||
|
||||
Add ` + "`--completionfile=/path/to/file`" + ` flag to set alternative
|
||||
file-path and name.
|
||||
|
||||
Logout and in again to reload the completion scripts,
|
||||
or just source them in directly:
|
||||
|
||||
$ . /etc/bash_completion`,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if autocompleteType != "bash" {
|
||||
return newUserError("Only Bash is supported for now")
|
||||
}
|
||||
|
||||
err := cmd.Root().GenBashCompletionFile(autocompleteTarget)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Bash completion file for Hugo saved to", autocompleteTarget)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
genautocompleteCmd.PersistentFlags().StringVarP(&autocompleteTarget, "completionfile", "", "/etc/bash_completion.d/hugo.sh", "Autocompletion file")
|
||||
genautocompleteCmd.PersistentFlags().StringVarP(&autocompleteType, "type", "", "bash", "Autocompletion type (currently only bash supported)")
|
||||
|
||||
// For bash-completion
|
||||
genautocompleteCmd.PersistentFlags().SetAnnotation("completionfile", cobra.BashCompFilenameExt, []string{})
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
+39
-53
@@ -18,81 +18,67 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/spf13/hugo/hugofs"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var _ cmder = (*genDocCmd)(nil)
|
||||
|
||||
type genDocCmd struct {
|
||||
gendocdir string
|
||||
*baseCmd
|
||||
}
|
||||
|
||||
func newGenDocCmd() *genDocCmd {
|
||||
const gendocFrontmatterTemplate = `---
|
||||
const gendocFrontmatterTemplate = `---
|
||||
date: %s
|
||||
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.
|
||||
var gendocdir string
|
||||
var gendocCmd = &cobra.Command{
|
||||
Use: "doc",
|
||||
Short: "Generate Markdown documentation for the Hugo CLI.",
|
||||
Long: `Generate Markdown documentation for the Hugo CLI.
|
||||
|
||||
This command is, mostly, used to create up-to-date documentation
|
||||
of Hugo's command-line interface for https://gohugo.io/.
|
||||
of Hugo's command-line interface for http://gohugo.io/.
|
||||
|
||||
It creates one Markdown file per command with front matter suitable
|
||||
for rendering in Hugo.`,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
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
|
||||
})
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) {
|
||||
gendocdir += helpers.FilePathSeparator
|
||||
}
|
||||
if found, _ := helpers.Exists(gendocdir, hugofs.Os()); !found {
|
||||
jww.FEEDBACK.Println("Directory", gendocdir, "does not exist, creating...")
|
||||
hugofs.Os().MkdirAll(gendocdir, 0777)
|
||||
}
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
prepender := func(filename string) string {
|
||||
name := filepath.Base(filename)
|
||||
base := strings.TrimSuffix(name, path.Ext(name))
|
||||
url := "/commands/" + strings.ToLower(base) + "/"
|
||||
return fmt.Sprintf(gendocFrontmatterTemplate, now, strings.Replace(base, "_", " ", -1), base, url)
|
||||
}
|
||||
|
||||
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) + "/"
|
||||
}
|
||||
|
||||
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.")
|
||||
jww.FEEDBACK.Println("Generating Hugo command-line documentation in", gendocdir, "...")
|
||||
doc.GenMarkdownTreeCustom(cmd.Root(), gendocdir, prepender, linkHandler)
|
||||
jww.FEEDBACK.Println("Done.")
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
|
||||
func init() {
|
||||
gendocCmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
|
||||
|
||||
// For bash-completion
|
||||
cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
|
||||
return cc
|
||||
gendocCmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
+30
-43
@@ -17,61 +17,48 @@ 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"
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/spf13/hugo/hugofs"
|
||||
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
|
||||
var genmandir string
|
||||
var genmanCmd = &cobra.Command{
|
||||
Use: "man",
|
||||
Short: "Generate man pages for the Hugo CLI",
|
||||
Long: `This command automatically generates up-to-date man pages of Hugo's
|
||||
command-line interface. By default, it creates the man page files
|
||||
in the "man" directory under the current directory.`,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
header := &doc.GenManHeader{
|
||||
Section: "1",
|
||||
Manual: "Hugo Manual",
|
||||
Source: fmt.Sprintf("Hugo %s", 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
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
header := &doc.GenManHeader{
|
||||
Section: "1",
|
||||
Manual: "Hugo Manual",
|
||||
Source: fmt.Sprintf("Hugo %s", helpers.HugoVersion()),
|
||||
}
|
||||
if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) {
|
||||
genmandir += helpers.FilePathSeparator
|
||||
}
|
||||
if found, _ := helpers.Exists(genmandir, hugofs.Os()); !found {
|
||||
jww.FEEDBACK.Println("Directory", genmandir, "does not exist, creating...")
|
||||
hugofs.Os().MkdirAll(genmandir, 0777)
|
||||
}
|
||||
cmd.Root().DisableAutoGenTag = true
|
||||
|
||||
jww.FEEDBACK.Println("Generating Hugo man pages in", cc.genmandir, "...")
|
||||
doc.GenManTree(cmd.Root(), header, cc.genmandir)
|
||||
jww.FEEDBACK.Println("Generating Hugo man pages in", genmandir, "...")
|
||||
doc.GenManTree(cmd.Root(), header, genmandir)
|
||||
|
||||
jww.FEEDBACK.Println("Done.")
|
||||
jww.FEEDBACK.Println("Done.")
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.genmandir, "dir", "man/", "the directory to write the man pages.")
|
||||
func init() {
|
||||
genmanCmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.")
|
||||
|
||||
// For bash-completion
|
||||
cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
|
||||
return cc
|
||||
genmanCmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package commands defines and implements command-line commands and flags
|
||||
// used by Hugo. Commands and flags are implemented using Cobra.
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
ansiEsc = "\u001B"
|
||||
clearLine = "\r\033[K"
|
||||
hideCursor = ansiEsc + "[?25l"
|
||||
showCursor = ansiEsc + "[?25h"
|
||||
)
|
||||
|
||||
type flagsToConfigHandler interface {
|
||||
flagsToConfig(cfg config.Provider)
|
||||
}
|
||||
|
||||
type cmder interface {
|
||||
flagsToConfigHandler
|
||||
getCommand() *cobra.Command
|
||||
}
|
||||
|
||||
// commandError is an error used to signal different error situations in command handling.
|
||||
type commandError struct {
|
||||
s string
|
||||
userError bool
|
||||
}
|
||||
|
||||
func (c commandError) Error() string {
|
||||
return c.s
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
+769
-1059
File diff suppressed because it is too large
Load Diff
@@ -1,206 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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))
|
||||
}
|
||||
@@ -13,13 +13,7 @@
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
// For time zone lookups on Windows without Go installed.
|
||||
// See #8892
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func init() {
|
||||
// This message to show to Windows users if Hugo is opened from explorer.exe
|
||||
@@ -29,5 +23,5 @@ func init() {
|
||||
|
||||
You need to open cmd.exe and run Hugo from there.
|
||||
|
||||
Visit https://gohugo.io/ for more information.`
|
||||
Visit http://gohugo.io/ for more information.`
|
||||
}
|
||||
|
||||
+186
-259
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 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.
|
||||
@@ -16,7 +16,7 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -24,97 +24,75 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"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/spf13/afero"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/spf13/hugo/hugofs"
|
||||
"github.com/spf13/hugo/hugolib"
|
||||
"github.com/spf13/hugo/parser"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var _ cmder = (*importCmd)(nil)
|
||||
|
||||
type importCmd struct {
|
||||
*baseCmd
|
||||
func init() {
|
||||
importCmd.AddCommand(importJekyllCmd)
|
||||
}
|
||||
|
||||
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.
|
||||
var importCmd = &cobra.Command{
|
||||
Use: "import",
|
||||
Short: "Import your site from others.",
|
||||
Long: `Import your site from other web site generators like Jekyll.
|
||||
|
||||
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
|
||||
RunE: nil,
|
||||
})
|
||||
|
||||
importJekyllCmd := &cobra.Command{
|
||||
Use: "jekyll",
|
||||
Short: "hugo import from Jekyll",
|
||||
Long: `hugo import from Jekyll.
|
||||
|
||||
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
|
||||
RunE: cc.importFromJekyll,
|
||||
}
|
||||
|
||||
importJekyllCmd.Flags().Bool("force", false, "allow import into non-empty target directory")
|
||||
|
||||
cc.cmd.AddCommand(importJekyllCmd)
|
||||
|
||||
return cc
|
||||
RunE: nil,
|
||||
}
|
||||
|
||||
func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
var importJekyllCmd = &cobra.Command{
|
||||
Use: "jekyll",
|
||||
Short: "hugo import from Jekyll",
|
||||
Long: `hugo import from Jekyll.
|
||||
|
||||
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
|
||||
RunE: importFromJekyll,
|
||||
}
|
||||
|
||||
func init() {
|
||||
importJekyllCmd.Flags().Bool("force", false, "Allow import into non-empty target directory")
|
||||
}
|
||||
|
||||
func importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
jww.SetLogThreshold(jww.LevelTrace)
|
||||
jww.SetStdoutThreshold(jww.LevelWarn)
|
||||
|
||||
if len(args) < 2 {
|
||||
return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
|
||||
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])
|
||||
return newUserError("Path error:", args[0])
|
||||
}
|
||||
|
||||
targetDir, err := filepath.Abs(filepath.Clean(args[1]))
|
||||
if err != nil {
|
||||
return newUserError("path error:", args[1])
|
||||
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")
|
||||
return newUserError("Target path should not be inside the Jekyll root, aborting.")
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := createSiteFromJekyll(jekyllRoot, targetDir, forceImport); err != nil {
|
||||
return newUserError(err)
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Importing...")
|
||||
|
||||
fileCount := 0
|
||||
callback := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
callback := func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -125,17 +103,17 @@ func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
|
||||
relPath, err := filepath.Rel(jekyllRoot, path)
|
||||
if err != nil {
|
||||
return newUserError("get rel path error:", path)
|
||||
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))
|
||||
case strings.HasPrefix(relPath, "_posts/"):
|
||||
relPath = "content/post" + relPath[len("_posts"):]
|
||||
case strings.HasPrefix(relPath, "_drafts/"):
|
||||
relPath = "content/draft" + relPath[len("_drafts"):]
|
||||
draft = true
|
||||
default:
|
||||
return nil
|
||||
@@ -145,14 +123,11 @@ func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
err = helpers.SymbolicWalk(hugofs.Os(), jekyllRoot, callback)
|
||||
|
||||
if 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")
|
||||
@@ -161,60 +136,40 @@ func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
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 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{}
|
||||
// TODO: Consider calling doNewSite() instead?
|
||||
func createSiteFromJekyll(jekyllRoot, targetDir string, force bool) error {
|
||||
fs := hugofs.Source()
|
||||
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")
|
||||
return errors.New("Target path \"" + targetDir + "\" already exists but not a directory")
|
||||
}
|
||||
|
||||
isEmpty, _ := helpers.IsEmpty(targetDir, fs)
|
||||
|
||||
if !isEmpty && !force {
|
||||
return errors.New("target path \"" + targetDir + "\" exists and is not empty")
|
||||
return errors.New("Target path \"" + targetDir + "\" already exists and is not empty")
|
||||
}
|
||||
}
|
||||
|
||||
jekyllConfig := i.loadJekyllConfig(fs, jekyllRoot)
|
||||
jekyllConfig := loadJekyllConfig(jekyllRoot)
|
||||
|
||||
// Crude test to make sure at least one of _drafts/ and _posts/ exists
|
||||
// and is not empty.
|
||||
hasPostsOrDrafts := false
|
||||
postsDir := filepath.Join(jekyllRoot, "_posts")
|
||||
draftsDir := filepath.Join(jekyllRoot, "_drafts")
|
||||
for _, d := range []string{postsDir, draftsDir} {
|
||||
if exists, _ := helpers.Exists(d, fs); exists {
|
||||
if isDir, _ := helpers.IsDir(d, fs); isDir {
|
||||
if isEmpty, _ := helpers.IsEmpty(d, fs); !isEmpty {
|
||||
hasPostsOrDrafts = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasPostsOrDrafts {
|
||||
return errors.New("Your Jekyll root contains neither posts nor drafts, aborting.")
|
||||
}
|
||||
|
||||
mkdir(targetDir, "layouts")
|
||||
mkdir(targetDir, "content")
|
||||
@@ -223,14 +178,15 @@ func (i *importCmd) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPos
|
||||
mkdir(targetDir, "data")
|
||||
mkdir(targetDir, "themes")
|
||||
|
||||
i.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
|
||||
createConfigFromJekyll(targetDir, "yaml", jekyllConfig)
|
||||
|
||||
i.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
|
||||
copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]any {
|
||||
func loadJekyllConfig(jekyllRoot string) map[string]interface{} {
|
||||
fs := hugofs.Source()
|
||||
path := filepath.Join(jekyllRoot, "_config.yml")
|
||||
|
||||
exists, err := helpers.Exists(path, fs)
|
||||
@@ -248,19 +204,21 @@ func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]
|
||||
defer f.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(f)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
|
||||
c, err := parser.HandleYAMLMetaData(b)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c
|
||||
return c.(map[string]interface{})
|
||||
}
|
||||
|
||||
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
|
||||
func createConfigFromJekyll(inpath string, kind string, jekyllConfig map[string]interface{}) (err error) {
|
||||
title := "My New Hugo Site"
|
||||
baseURL := "http://example.org/"
|
||||
|
||||
@@ -280,26 +238,87 @@ func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind meta
|
||||
}
|
||||
}
|
||||
|
||||
in := map[string]any{
|
||||
in := map[string]interface{}{
|
||||
"baseURL": baseURL,
|
||||
"title": title,
|
||||
"languageCode": "en-us",
|
||||
"disablePathToLower": true,
|
||||
}
|
||||
kind = parser.FormatSanitize(kind)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = parser.InterfaceToConfig(in, kind, &buf)
|
||||
by, err := parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "config."+string(kind)), &buf, fs)
|
||||
err = helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), bytes.NewReader(by), hugofs.Source())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) {
|
||||
fs := hugofs.Os
|
||||
func copyFile(source string, dest string) error {
|
||||
sf, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sf.Close()
|
||||
df, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer df.Close()
|
||||
_, err = io.Copy(df, sf)
|
||||
if err == nil {
|
||||
si, err := os.Stat(source)
|
||||
if err != nil {
|
||||
err = os.Chmod(dest, si.Mode())
|
||||
|
||||
fi, err := fs.Stat(jekyllRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDir(source string, dest string) error {
|
||||
fi, err := os.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return errors.New(source + " is not a directory")
|
||||
}
|
||||
err = os.MkdirAll(dest, fi.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := ioutil.ReadDir(source)
|
||||
for _, entry := range entries {
|
||||
sfp := filepath.Join(source, entry.Name())
|
||||
dfp := filepath.Join(dest, entry.Name())
|
||||
if entry.IsDir() {
|
||||
err = copyDir(sfp, dfp)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
} else {
|
||||
err = copyFile(sfp, dfp)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyJekyllFilesAndFolders(jekyllRoot string, dest string) error {
|
||||
fi, err := os.Stat(jekyllRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -311,28 +330,20 @@ func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPos
|
||||
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)
|
||||
}
|
||||
err = copyDir(sfp, dfp)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lowerEntryName := strings.ToLower(entry.Name())
|
||||
exceptSuffix := []string{
|
||||
".md", ".markdown", ".html", ".htm",
|
||||
".xml", ".textile", "rakefile", "gemfile", ".lock",
|
||||
}
|
||||
exceptSuffix := []string{".md", ".markdown", ".html", ".htm",
|
||||
".xml", ".textile", "rakefile", "gemfile", ".lock"}
|
||||
isExcept := false
|
||||
for _, suffix := range exceptSuffix {
|
||||
if strings.HasSuffix(lowerEntryName, suffix) {
|
||||
@@ -342,7 +353,7 @@ func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPos
|
||||
}
|
||||
|
||||
if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' {
|
||||
err = hugio.CopyFile(fs, sfp, dfp)
|
||||
err = copyFile(sfp, dfp)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
@@ -357,12 +368,12 @@ 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")
|
||||
return time.Now(), "", errors.New("filename not match")
|
||||
}
|
||||
|
||||
postDate, err := time.Parse("2006-1-2", r[0][1])
|
||||
postDate, err := time.Parse("2006-01-02", r[0][1])
|
||||
if err != nil {
|
||||
return htime.Now(), "", err
|
||||
return time.Now(), "", err
|
||||
}
|
||||
|
||||
postName := r[0][2]
|
||||
@@ -392,34 +403,47 @@ func convertJekyllPost(path, relPath, targetDir string, draft bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
pf, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(contentBytes))
|
||||
psr, err := parser.ReadFrom(bytes.NewReader(contentBytes))
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Parse file error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
newmetadata, err := convertJekyllMetaData(pf.FrontMatter, postName, postDate, draft)
|
||||
metadata, err := psr.Metadata()
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Processing file error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
newmetadata, err := convertJekyllMetaData(metadata, postName, postDate, draft)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Convert metadata error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := convertJekyllContent(newmetadata, string(pf.Content))
|
||||
jww.TRACE.Println(newmetadata)
|
||||
content := convertJekyllContent(newmetadata, string(psr.Content()))
|
||||
|
||||
page, err := hugolib.NewPage(filename)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Converting Jekyll error:", path)
|
||||
jww.ERROR.Println("New page error", filename)
|
||||
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)
|
||||
}
|
||||
page.SetDir(targetParentDir)
|
||||
page.SetSourceContent([]byte(content))
|
||||
page.SetSourceMetaData(newmetadata, parser.FormatToLeadRune("yaml"))
|
||||
page.SaveSourceAs(targetFile)
|
||||
|
||||
jww.TRACE.Println("Target file:", targetFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
|
||||
metadata, err := maps.ToStringMapE(m)
|
||||
func convertJekyllMetaData(m interface{}, postName string, postDate time.Time, draft bool) (interface{}, error) {
|
||||
url := postDate.Format("/2006/01/02/") + postName + "/"
|
||||
|
||||
metadata, err := cast.ToStringMapE(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -436,7 +460,7 @@ func convertJekyllMetaData(m any, postName string, postDate time.Time, draft boo
|
||||
delete(metadata, key)
|
||||
case "permalink":
|
||||
if str, ok := value.(string); ok {
|
||||
metadata["url"] = str
|
||||
url = str
|
||||
}
|
||||
delete(metadata, key)
|
||||
case "category":
|
||||
@@ -465,13 +489,14 @@ func convertJekyllMetaData(m any, postName string, postDate time.Time, draft boo
|
||||
|
||||
}
|
||||
|
||||
metadata["url"] = url
|
||||
metadata["date"] = postDate.Format(time.RFC3339)
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func convertJekyllContent(m any, content string) (string, error) {
|
||||
metadata, _ := maps.ToStringMapE(m)
|
||||
func convertJekyllContent(m interface{}, content string) string {
|
||||
metadata, _ := cast.ToStringMapE(m)
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
var resultLines []string
|
||||
@@ -492,8 +517,9 @@ func convertJekyllContent(m any, content string) (string, error) {
|
||||
re *regexp.Regexp
|
||||
replace string
|
||||
}{
|
||||
{regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"},
|
||||
{regexp.MustCompile("<!-- more -->"), "<!--more-->"},
|
||||
{regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"},
|
||||
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), "{{< highlight $1 >}}"},
|
||||
{regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"},
|
||||
}
|
||||
|
||||
@@ -501,104 +527,5 @@ func convertJekyllContent(m any, content string) (string, error) {
|
||||
content = replace.re.ReplaceAllString(content, replace.replace)
|
||||
}
|
||||
|
||||
replaceListFunc := []struct {
|
||||
re *regexp.Regexp
|
||||
replace func(string) string
|
||||
}{
|
||||
// Octopress image tag: http://octopress.org/docs/plugins/image-tag/
|
||||
{regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), replaceImageTag},
|
||||
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), 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 replaceHighlightTag(match string) string {
|
||||
r := regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`)
|
||||
parts := r.FindStringSubmatch(match)
|
||||
lastQuote := rune(0)
|
||||
f := func(c rune) bool {
|
||||
switch {
|
||||
case c == lastQuote:
|
||||
lastQuote = rune(0)
|
||||
return false
|
||||
case lastQuote != rune(0):
|
||||
return false
|
||||
case unicode.In(c, unicode.Quotation_Mark):
|
||||
lastQuote = c
|
||||
return false
|
||||
default:
|
||||
return unicode.IsSpace(c)
|
||||
}
|
||||
}
|
||||
// splitting string by space but considering quoted section
|
||||
items := strings.FieldsFunc(parts[1], f)
|
||||
|
||||
result := bytes.NewBufferString("{{< highlight ")
|
||||
result.WriteString(items[0]) // language
|
||||
options := items[1:]
|
||||
for i, opt := range options {
|
||||
opt = strings.Replace(opt, "\"", "", -1)
|
||||
if opt == "linenos" {
|
||||
opt = "linenos=table"
|
||||
}
|
||||
if i == 0 {
|
||||
opt = " \"" + opt
|
||||
}
|
||||
if i < len(options)-1 {
|
||||
opt += ","
|
||||
} else if i == len(options)-1 {
|
||||
opt += "\""
|
||||
}
|
||||
result.WriteString(opt)
|
||||
}
|
||||
|
||||
result.WriteString(" >}}")
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func replaceImageTag(match string) string {
|
||||
r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`)
|
||||
result := bytes.NewBufferString("{{< figure ")
|
||||
parts := r.FindStringSubmatch(match)
|
||||
// Index 0 is the entire string, ignore
|
||||
replaceOptionalPart(result, "class", parts[1])
|
||||
replaceOptionalPart(result, "src", parts[2])
|
||||
replaceOptionalPart(result, "width", parts[3])
|
||||
replaceOptionalPart(result, "height", parts[4])
|
||||
// title + alt
|
||||
part := parts[5]
|
||||
if len(part) > 0 {
|
||||
splits := strings.Split(part, "'")
|
||||
lenSplits := len(splits)
|
||||
if lenSplits == 1 {
|
||||
replaceOptionalPart(result, "title", splits[0])
|
||||
} else if lenSplits == 3 {
|
||||
replaceOptionalPart(result, "title", splits[1])
|
||||
} else if lenSplits == 5 {
|
||||
replaceOptionalPart(result, "title", splits[1])
|
||||
replaceOptionalPart(result, "alt", splits[3])
|
||||
}
|
||||
}
|
||||
result.WriteString(">}}")
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) {
|
||||
if len(part) > 0 {
|
||||
buffer.WriteString(partName + "=\"" + part + "\" ")
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
+34
-107
@@ -15,14 +15,12 @@ package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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",
|
||||
@@ -38,140 +36,69 @@ func TestParseJekyllFilename(t *testing.T) {
|
||||
|
||||
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)
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, expectResult[i].postDate.Format("2006-01-02"), postDate.Format("2006-01-02"))
|
||||
assert.Equal(t, expectResult[i].postName, postName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertJekyllMetadata(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
testDataList := []struct {
|
||||
metadata any
|
||||
metadata interface{}
|
||||
postName string
|
||||
postDate time.Time
|
||||
draft bool
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
map[any]any{},
|
||||
{map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/2015/10/01/testPost/"}`},
|
||||
{map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true,
|
||||
`{"date":"2015-10-01T00:00:00Z","draft":true,"url":"/2015/10/01/testPost/"}`},
|
||||
{map[interface{}]interface{}{"Permalink": "/permalink.html", "layout": "post"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z"}`,
|
||||
},
|
||||
{
|
||||
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"},
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`},
|
||||
{map[interface{}]interface{}{"permalink": "/permalink.html"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
|
||||
},
|
||||
{
|
||||
map[any]any{"permalink": "/permalink.html"},
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`},
|
||||
{map[interface{}]interface{}{"category": nil, "permalink": 123},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
|
||||
},
|
||||
{
|
||||
map[any]any{"category": nil, "permalink": 123},
|
||||
`{"date":"2015-10-01T00:00:00Z","url":"/2015/10/01/testPost/"}`},
|
||||
{map[interface{}]interface{}{"Excerpt_Separator": "sep"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"date":"2015-10-01T00:00:00Z"}`,
|
||||
},
|
||||
{
|
||||
map[any]any{"Excerpt_Separator": "sep"},
|
||||
`{"date":"2015-10-01T00:00:00Z","excerpt_separator":"sep","url":"/2015/10/01/testPost/"}`},
|
||||
{map[interface{}]interface{}{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"},
|
||||
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
|
||||
`{"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"}`,
|
||||
},
|
||||
`{"Others":"Goods","categories":["book"],"date":"2015-10-01T12:13:11Z","url":"/2015/10/01/testPost/"}`},
|
||||
}
|
||||
|
||||
for _, data := range testDataList {
|
||||
result, err := convertJekyllMetaData(data.metadata, data.postName, data.postDate, data.draft)
|
||||
c.Assert(err, qt.IsNil)
|
||||
assert.Equal(t, nil, err)
|
||||
jsonResult, err := json.Marshal(result)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(jsonResult), qt.Equals, data.expect)
|
||||
assert.Equal(t, nil, err)
|
||||
assert.Equal(t, data.expect, string(jsonResult))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertJekyllContent(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
testDataList := []struct {
|
||||
metadata any
|
||||
metadata interface{}
|
||||
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{},
|
||||
{map[interface{}]interface{}{},
|
||||
`Test content\n<!-- more -->\npart2 content`, `Test content\n<!--more-->\npart2 content`},
|
||||
{map[interface{}]interface{}{"excerpt_separator": "<!--sep-->"},
|
||||
`Test content\n<!--sep-->\npart2 content`, `Test content\n<!--more-->\npart2 content`},
|
||||
{map[interface{}]interface{}{}, "{% raw %}text{% endraw %}", "text"},
|
||||
{map[interface{}]interface{}{}, "{%raw%} text2 {%endraw %}", "text2"},
|
||||
{map[interface{}]interface{}{},
|
||||
"{% highlight go %}\nvar s int\n{% endhighlight %}",
|
||||
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}"},
|
||||
}
|
||||
|
||||
for _, data := range testDataList {
|
||||
result, err := convertJekyllContent(data.metadata, data.content)
|
||||
c.Assert(result, qt.Equals, data.expect)
|
||||
c.Assert(err, qt.IsNil)
|
||||
result := convertJekyllContent(data.metadata, data.content)
|
||||
assert.Equal(t, data.expect, result)
|
||||
}
|
||||
}
|
||||
|
||||
+47
-45
@@ -1,4 +1,18 @@
|
||||
// Copyright 2018 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.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build darwin
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -20,65 +34,53 @@ import (
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var _ cmder = (*limitCmd)(nil)
|
||||
|
||||
type limitCmd struct {
|
||||
*baseCmd
|
||||
func init() {
|
||||
checkCmd.AddCommand(limit)
|
||||
}
|
||||
|
||||
func newLimitCmd() *limitCmd {
|
||||
ccmd := &cobra.Command{
|
||||
Use: "ulimit",
|
||||
Short: "Check system ulimit settings",
|
||||
Long: `Hugo will inspect the current ulimit settings on the system.
|
||||
var limit = &cobra.Command{
|
||||
Use: "ulimit",
|
||||
Short: "Check system ulimit settings",
|
||||
Long: `Hugo will inspect the current ulimit settings on the system.
|
||||
This is primarily to ensure that Hugo can watch enough files on some OSs`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var rLimit syscall.Rlimit
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
return newSystemError("Error Getting rlimit ", err)
|
||||
}
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var rLimit syscall.Rlimit
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
return newSystemError("Error Getting Rlimit ", err)
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Current rLimit:", rLimit)
|
||||
jww.FEEDBACK.Println("Current rLimit:", rLimit)
|
||||
|
||||
if rLimit.Cur >= newRlimit {
|
||||
return nil
|
||||
}
|
||||
jww.FEEDBACK.Println("Attempting to increase limit")
|
||||
rLimit.Max = 999999
|
||||
rLimit.Cur = 999999
|
||||
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
return newSystemError("Error Setting rLimit ", err)
|
||||
}
|
||||
err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
return newSystemError("Error Getting rLimit ", err)
|
||||
}
|
||||
jww.FEEDBACK.Println("rLimit after change:", rLimit)
|
||||
|
||||
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)}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
jww.ERROR.Println("Unable to obtain rLimit", err)
|
||||
}
|
||||
if rLimit.Cur < newRlimit {
|
||||
rLimit.Cur = newRlimit
|
||||
if rLimit.Cur < rLimit.Max {
|
||||
rLimit.Max = 999999
|
||||
rLimit.Cur = 999999
|
||||
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)
|
||||
jww.ERROR.Println("Unable to increase number of open files limit", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,8 +11,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !darwin
|
||||
// +build !darwin
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
|
||||
+119
-185
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 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,197 +14,131 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/hugo/hugolib"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var _ cmder = (*listCmd)(nil)
|
||||
|
||||
type listCmd struct {
|
||||
*baseBuilderCmd
|
||||
func init() {
|
||||
listCmd.AddCommand(listDraftsCmd)
|
||||
listCmd.AddCommand(listFutureCmd)
|
||||
listCmd.AddCommand(listExpiredCmd)
|
||||
listCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from")
|
||||
listCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, true, false, &lc.hugoBuilderCommon, lc, cfgInit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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.
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Listing out various types of content",
|
||||
Long: `Listing out various types of content.
|
||||
|
||||
List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
RunE: nil,
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
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
|
||||
RunE: nil,
|
||||
}
|
||||
|
||||
var listDraftsCmd = &cobra.Command{
|
||||
Use: "drafts",
|
||||
Short: "List all drafts",
|
||||
Long: `List all of the drafts in your content directory.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if err := InitializeConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viper.Set("buildDrafts", true)
|
||||
|
||||
sites, err := hugolib.NewHugoSitesFromConfiguration()
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error creating sites", err)
|
||||
}
|
||||
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
|
||||
for _, p := range sites.Pages() {
|
||||
if p.IsDraft() {
|
||||
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
var listFutureCmd = &cobra.Command{
|
||||
Use: "future",
|
||||
Short: "List all posts dated in the future",
|
||||
Long: `List all of the posts in your content directory which will be
|
||||
posted in the future.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if err := InitializeConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viper.Set("buildFuture", true)
|
||||
|
||||
sites, err := hugolib.NewHugoSitesFromConfiguration()
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error creating sites", err)
|
||||
}
|
||||
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
|
||||
for _, p := range sites.Pages() {
|
||||
if p.IsFuture() {
|
||||
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
var listExpiredCmd = &cobra.Command{
|
||||
Use: "expired",
|
||||
Short: "List all posts already expired",
|
||||
Long: `List all of the posts in your content directory which has already
|
||||
expired.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if err := InitializeConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viper.Set("buildExpired", true)
|
||||
|
||||
sites, err := hugolib.NewHugoSitesFromConfiguration()
|
||||
|
||||
if err != nil {
|
||||
return newSystemError("Error creating sites", err)
|
||||
}
|
||||
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return newSystemError("Error Processing Source Content", err)
|
||||
}
|
||||
|
||||
for _, p := range sites.Pages() {
|
||||
if p.IsExpired() {
|
||||
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.Print the version number of Hug
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Print the site configuration",
|
||||
Long: `Print the site configuration, both default and custom settings.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.RunE = config
|
||||
}
|
||||
|
||||
func config(cmd *cobra.Command, args []string) error {
|
||||
if err := InitializeConfig(configCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allSettings := viper.AllSettings()
|
||||
|
||||
var separator string
|
||||
if allSettings["metadataformat"] == "toml" {
|
||||
separator = " = "
|
||||
} else {
|
||||
separator = ": "
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for k := range allSettings {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
kv := reflect.ValueOf(allSettings[k])
|
||||
if kv.Kind() == reflect.String {
|
||||
jww.FEEDBACK.Printf("%s%s\"%+v\"\n", k, separator, allSettings[k])
|
||||
} else {
|
||||
jww.FEEDBACK.Printf("%s%s%+v\n", k, separator, allSettings[k])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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/",
|
||||
})
|
||||
}
|
||||
-293
@@ -1,293 +0,0 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
See https://gohugo.io/hugo-modules/ for more information.
|
||||
|
||||
`
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "mod",
|
||||
Short: "Various Hugo Modules helpers.",
|
||||
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".
|
||||
|
||||
` + commonUsage,
|
||||
|
||||
RunE: nil,
|
||||
}
|
||||
|
||||
cmd.AddCommand(newModNPMCmd(c))
|
||||
|
||||
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:
|
||||
|
||||
Install the latest version possible for a given module:
|
||||
|
||||
hugo mod get github.com/gohugoio/testshortcodes
|
||||
|
||||
Install a specific version:
|
||||
|
||||
hugo mod get github.com/gohugoio/testshortcodes@v0.3.0
|
||||
|
||||
Install the latest versions of all 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.
|
||||
` + 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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
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)
|
||||
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...)
|
||||
})
|
||||
},
|
||||
},
|
||||
&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
|
||||
}
|
||||
|
||||
func (c *modCmd) withModsClient(failOnMissingConfig bool, f func(*modules.Client) error) error {
|
||||
com, err := c.initConfig(failOnMissingConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f(com.hugo().ModulesClient)
|
||||
}
|
||||
|
||||
func (c *modCmd) withHugo(f func(*hugolib.HugoSites) error) error {
|
||||
com, err := c.initConfig(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f(com.hugo())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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
|
||||
}
|
||||
+280
-54
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2016 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,72 +15,255 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/create"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/hugo/create"
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/spf13/hugo/hugofs"
|
||||
"github.com/spf13/hugo/parser"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var _ cmder = (*newCmd)(nil)
|
||||
|
||||
type newCmd struct {
|
||||
var (
|
||||
configFormat string
|
||||
contentEditor string
|
||||
contentType string
|
||||
)
|
||||
|
||||
func init() {
|
||||
newSiteCmd.Flags().StringVarP(&configFormat, "format", "f", "toml", "config & frontmatter format")
|
||||
newSiteCmd.Flags().Bool("force", false, "Init inside non-empty directory")
|
||||
newCmd.Flags().StringVarP(&configFormat, "format", "f", "toml", "frontmatter format")
|
||||
newCmd.Flags().StringVarP(&contentType, "kind", "k", "", "Content type to create")
|
||||
newCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from")
|
||||
newCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
newCmd.Flags().StringVar(&contentEditor, "editor", "", "edit new content with this editor, if provided")
|
||||
|
||||
newCmd.AddCommand(newSiteCmd)
|
||||
newCmd.AddCommand(newThemeCmd)
|
||||
|
||||
*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.
|
||||
var newCmd = &cobra.Command{
|
||||
Use: "new [path]",
|
||||
Short: "Create new content for your site",
|
||||
Long: `Create a new content file and automatically set the date and title.
|
||||
It will guess which kind of file to create based on the path provided.
|
||||
|
||||
You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
If archetypes are provided in your theme or site, they will be used.`,
|
||||
|
||||
Ensure you run this within the root directory of your site.`,
|
||||
}
|
||||
|
||||
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
|
||||
RunE: NewContent,
|
||||
}
|
||||
|
||||
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
|
||||
var newSiteCmd = &cobra.Command{
|
||||
Use: "site [path]",
|
||||
Short: "Create a new site (skeleton)",
|
||||
Long: `Create a new site in the provided directory.
|
||||
The new site will have the correct structure, but no content or theme yet.
|
||||
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
RunE: NewSite,
|
||||
}
|
||||
|
||||
var newThemeCmd = &cobra.Command{
|
||||
Use: "theme [name]",
|
||||
Short: "Create a new theme",
|
||||
Long: `Create a new theme (skeleton) called [name] in the current directory.
|
||||
New theme is a skeleton. Please add content to the touched files. Add your
|
||||
name to the copyright line in the license and adjust the theme.toml file
|
||||
as you see fit.`,
|
||||
RunE: NewTheme,
|
||||
}
|
||||
|
||||
// NewContent adds new content to a Hugo site.
|
||||
func NewContent(cmd *cobra.Command, args []string) error {
|
||||
if err := InitializeConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, true, false, &n.hugoBuilderCommon, n, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
if flagChanged(cmd.Flags(), "format") {
|
||||
viper.Set("metaDataFormat", configFormat)
|
||||
}
|
||||
|
||||
if flagChanged(cmd.Flags(), "editor") {
|
||||
viper.Set("newContentEditor", contentEditor)
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
|
||||
return create.NewContent(c.hugo(), n.contentType, args[0])
|
||||
createpath := args[0]
|
||||
|
||||
var kind string
|
||||
|
||||
createpath, kind = newContentPathSection(createpath)
|
||||
|
||||
if contentType != "" {
|
||||
kind = contentType
|
||||
}
|
||||
|
||||
return create.NewContent(hugofs.Source(), kind, createpath)
|
||||
}
|
||||
|
||||
func doNewSite(basepath string, force bool) error {
|
||||
dirs := []string{
|
||||
filepath.Join(basepath, "layouts"),
|
||||
filepath.Join(basepath, "content"),
|
||||
filepath.Join(basepath, "archetypes"),
|
||||
filepath.Join(basepath, "static"),
|
||||
filepath.Join(basepath, "data"),
|
||||
filepath.Join(basepath, "themes"),
|
||||
}
|
||||
|
||||
if exists, _ := helpers.Exists(basepath, hugofs.Source()); exists {
|
||||
if isDir, _ := helpers.IsDir(basepath, hugofs.Source()); !isDir {
|
||||
return errors.New(basepath + " already exists but not a directory")
|
||||
}
|
||||
|
||||
isEmpty, _ := helpers.IsEmpty(basepath, hugofs.Source())
|
||||
|
||||
switch {
|
||||
case !isEmpty && !force:
|
||||
return errors.New(basepath + " already exists and is not empty")
|
||||
|
||||
case !isEmpty && force:
|
||||
all := append(dirs, filepath.Join(basepath, "config."+configFormat))
|
||||
for _, path := range all {
|
||||
if exists, _ := helpers.Exists(path, hugofs.Source()); exists {
|
||||
return errors.New(path + " already exists")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
hugofs.Source().MkdirAll(dir, 0777)
|
||||
}
|
||||
|
||||
createConfig(basepath, configFormat)
|
||||
|
||||
jww.FEEDBACK.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", basepath)
|
||||
jww.FEEDBACK.Println(nextStepsText())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// NewSite creates a new Hugo site and initializes a structured Hugo directory.
|
||||
func NewSite(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
|
||||
createpath, err := filepath.Abs(filepath.Clean(args[0]))
|
||||
if err != nil {
|
||||
return newUserError(err)
|
||||
}
|
||||
|
||||
forceNew, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
return doNewSite(createpath, forceNew)
|
||||
}
|
||||
|
||||
// NewTheme creates a new Hugo theme.
|
||||
func NewTheme(cmd *cobra.Command, args []string) error {
|
||||
if err := InitializeConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
|
||||
return newUserError("theme name needs to be provided")
|
||||
}
|
||||
|
||||
createpath := helpers.AbsPathify(filepath.Join(viper.GetString("themesDir"), args[0]))
|
||||
jww.INFO.Println("creating theme at", createpath)
|
||||
|
||||
if x, _ := helpers.Exists(createpath, hugofs.Source()); x {
|
||||
return newUserError(createpath, "already exists")
|
||||
}
|
||||
|
||||
mkdir(createpath, "layouts", "_default")
|
||||
mkdir(createpath, "layouts", "partials")
|
||||
|
||||
touchFile(createpath, "layouts", "index.html")
|
||||
touchFile(createpath, "layouts", "404.html")
|
||||
touchFile(createpath, "layouts", "_default", "list.html")
|
||||
touchFile(createpath, "layouts", "_default", "single.html")
|
||||
|
||||
touchFile(createpath, "layouts", "partials", "header.html")
|
||||
touchFile(createpath, "layouts", "partials", "footer.html")
|
||||
|
||||
mkdir(createpath, "archetypes")
|
||||
|
||||
archDefault := []byte("+++\n+++\n")
|
||||
|
||||
err := helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), hugofs.Source())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mkdir(createpath, "static", "js")
|
||||
mkdir(createpath, "static", "css")
|
||||
|
||||
by := []byte(`The MIT License (MIT)
|
||||
|
||||
Copyright (c) ` + time.Now().Format("2006") + ` YOUR_NAME_HERE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
`)
|
||||
|
||||
err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE.md"), bytes.NewReader(by), hugofs.Source())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createThemeMD(createpath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mkdir(x ...string) {
|
||||
@@ -92,35 +275,78 @@ func mkdir(x ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func touchFile(fs afero.Fs, x ...string) {
|
||||
func touchFile(x ...string) {
|
||||
inpath := filepath.Join(x...)
|
||||
mkdir(filepath.Dir(inpath))
|
||||
err := helpers.WriteToDisk(inpath, bytes.NewReader([]byte{}), fs)
|
||||
err := helpers.WriteToDisk(inpath, bytes.NewReader([]byte{}), hugofs.Source())
|
||||
if err != nil {
|
||||
jww.FATAL.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
func createThemeMD(inpath string) (err error) {
|
||||
|
||||
if h != nil {
|
||||
for _, dir := range h.BaseFs.Content.Dirs {
|
||||
createpath = strings.TrimPrefix(createpath, dir.Meta().Filename)
|
||||
}
|
||||
by := []byte(`# theme.toml template for a Hugo theme
|
||||
# See https://github.com/spf13/hugoThemes#themetoml for an example
|
||||
|
||||
name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `"
|
||||
license = "MIT"
|
||||
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE.md"
|
||||
description = ""
|
||||
homepage = "http://siteforthistheme.com/"
|
||||
tags = ["", ""]
|
||||
features = ["", ""]
|
||||
min_version = 0.18
|
||||
|
||||
[author]
|
||||
name = ""
|
||||
homepage = ""
|
||||
|
||||
# If porting an existing theme
|
||||
[original]
|
||||
name = ""
|
||||
homepage = ""
|
||||
repo = ""
|
||||
`)
|
||||
|
||||
err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), hugofs.Source())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newContentPathSection(path string) (string, string) {
|
||||
// Forward slashes is used in all examples. Convert if needed.
|
||||
// Issue #1133
|
||||
createpath := strings.Replace(path, "/", helpers.FilePathSeparator, -1)
|
||||
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]
|
||||
}
|
||||
|
||||
section = helpers.GuessSection(createpath)
|
||||
}
|
||||
|
||||
return createpath, section
|
||||
}
|
||||
|
||||
func createConfig(inpath string, kind string) (err error) {
|
||||
in := map[string]interface{}{
|
||||
"baseurl": "http://example.org/",
|
||||
"title": "My New Hugo Site",
|
||||
"languageCode": "en-us",
|
||||
}
|
||||
kind = parser.FormatSanitize(kind)
|
||||
|
||||
by, err := parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), bytes.NewReader(by), hugofs.Source())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
// Issue #1133
|
||||
func TestNewContentPathSectionWithForwardSlashes(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
p, s := newContentPathSection(nil, "/post/new.md")
|
||||
c.Assert(p, qt.Equals, filepath.FromSlash("/post/new.md"))
|
||||
c.Assert(s, qt.Equals, "post")
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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,102 @@
|
||||
// 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 (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/hugo/hugofs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Issue #1133
|
||||
func TestNewContentPathSectionWithForwardSlashes(t *testing.T) {
|
||||
p, s := newContentPathSection("/post/new.md")
|
||||
assert.Equal(t, filepath.FromSlash("/post/new.md"), p)
|
||||
assert.Equal(t, "post", s)
|
||||
}
|
||||
|
||||
func checkNewSiteInited(basepath string, t *testing.T) {
|
||||
paths := []string{
|
||||
filepath.Join(basepath, "layouts"),
|
||||
filepath.Join(basepath, "content"),
|
||||
filepath.Join(basepath, "archetypes"),
|
||||
filepath.Join(basepath, "static"),
|
||||
filepath.Join(basepath, "data"),
|
||||
filepath.Join(basepath, "config.toml"),
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
_, err := hugofs.Source().Stat(path)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoNewSite(t *testing.T) {
|
||||
basepath := filepath.Join(os.TempDir(), "blog")
|
||||
hugofs.InitMemFs()
|
||||
err := doNewSite(basepath, false)
|
||||
assert.Nil(t, err)
|
||||
|
||||
checkNewSiteInited(basepath, t)
|
||||
}
|
||||
|
||||
func TestDoNewSite_noerror_base_exists_but_empty(t *testing.T) {
|
||||
basepath := filepath.Join(os.TempDir(), "blog")
|
||||
hugofs.InitMemFs()
|
||||
hugofs.Source().MkdirAll(basepath, 777)
|
||||
err := doNewSite(basepath, false)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestDoNewSite_error_base_exists(t *testing.T) {
|
||||
basepath := filepath.Join(os.TempDir(), "blog")
|
||||
hugofs.InitMemFs()
|
||||
hugofs.Source().MkdirAll(basepath, 777)
|
||||
hugofs.Source().Create(filepath.Join(basepath, "foo"))
|
||||
// Since the directory already exists and isn't empty, expect an error
|
||||
err := doNewSite(basepath, false)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestDoNewSite_force_empty_dir(t *testing.T) {
|
||||
basepath := filepath.Join(os.TempDir(), "blog")
|
||||
hugofs.InitMemFs()
|
||||
hugofs.Source().MkdirAll(basepath, 777)
|
||||
err := doNewSite(basepath, true)
|
||||
assert.Nil(t, err)
|
||||
|
||||
checkNewSiteInited(basepath, t)
|
||||
}
|
||||
|
||||
func TestDoNewSite_error_force_dir_inside_exists(t *testing.T) {
|
||||
basepath := filepath.Join(os.TempDir(), "blog")
|
||||
contentPath := filepath.Join(basepath, "content")
|
||||
hugofs.InitMemFs()
|
||||
hugofs.Source().MkdirAll(contentPath, 777)
|
||||
err := doNewSite(basepath, true)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestDoNewSite_error_force_config_inside_exists(t *testing.T) {
|
||||
basepath := filepath.Join(os.TempDir(), "blog")
|
||||
configPath := filepath.Join(basepath, "config.toml")
|
||||
hugofs.InitMemFs()
|
||||
hugofs.Source().MkdirAll(basepath, 777)
|
||||
hugofs.Source().Create(configPath)
|
||||
err := doNewSite(basepath, true)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//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
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
//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.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/releaser"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var _ cmder = (*releaseCommandeer)(nil)
|
||||
|
||||
type releaseCommandeer struct {
|
||||
cmd *cobra.Command
|
||||
|
||||
version string
|
||||
|
||||
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,21 +0,0 @@
|
||||
//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.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
func createReleaser() cmder {
|
||||
return &nilCommand{}
|
||||
}
|
||||
+129
-563
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 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,73 +14,42 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"mime"
|
||||
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/spf13/hugo/hugofs"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type serverCmd struct {
|
||||
// Can be used to stop the server. Useful in tests
|
||||
stop chan bool
|
||||
var (
|
||||
disableLiveReload bool
|
||||
renderToDisk bool
|
||||
serverAppend bool
|
||||
serverInterface string
|
||||
serverPort int
|
||||
serverWatch bool
|
||||
)
|
||||
|
||||
disableLiveReload bool
|
||||
navigateToChanged bool
|
||||
renderToDisk bool
|
||||
renderStaticToDisk bool
|
||||
serverAppend bool
|
||||
serverInterface string
|
||||
serverPort int
|
||||
liveReloadPort int
|
||||
serverWatch bool
|
||||
noHTTPCache bool
|
||||
//var serverCmdV *cobra.Command
|
||||
|
||||
disableFastRender bool
|
||||
disableBrowserError bool
|
||||
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newServerCmd() *serverCmd {
|
||||
return b.newServerCmdSignaled(nil)
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newServerCmdSignaled(stop chan bool) *serverCmd {
|
||||
cc := &serverCmd{stop: stop}
|
||||
|
||||
cc.baseBuilderCmd = b.newBuilderCmd(&cobra.Command{
|
||||
Use: "server",
|
||||
Aliases: []string{"serve"},
|
||||
Short: "A high performance webserver",
|
||||
Long: `Hugo provides its own webserver which builds and serves the site.
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Aliases: []string{"serve"},
|
||||
Short: "A high performance webserver",
|
||||
Long: `Hugo provides its own webserver which builds and serves the site.
|
||||
While hugo server is high performance, it is a webserver with limited options.
|
||||
Many run it in production, but the standard behavior is for people to use it
|
||||
in development and use a more full featured server such as Nginx or Caddy.
|
||||
@@ -92,32 +61,7 @@ By default hugo will also watch your files for any changes you make and
|
||||
automatically rebuild the site. It will then live reload any open browser pages
|
||||
and push the latest content to them. As most Hugo sites are built in a fraction
|
||||
of a second, you will be able to save and see your changes nearly instantly.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := cc.server(cmd, args)
|
||||
if err != nil && cc.stop != nil {
|
||||
cc.stop <- true
|
||||
}
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen")
|
||||
cc.cmd.Flags().IntVar(&cc.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)")
|
||||
cc.cmd.Flags().StringVarP(&cc.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
|
||||
cc.cmd.Flags().BoolVarP(&cc.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
|
||||
cc.cmd.Flags().BoolVar(&cc.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching")
|
||||
cc.cmd.Flags().BoolVarP(&cc.serverAppend, "appendPort", "", true, "append port to baseURL")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
|
||||
cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
|
||||
cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "serve all files from disk (default is from memory)")
|
||||
cc.cmd.Flags().BoolVar(&cc.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
|
||||
|
||||
cc.cmd.Flags().String("memstats", "", "log memory usage to this file")
|
||||
cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
|
||||
|
||||
return cc
|
||||
//RunE: server,
|
||||
}
|
||||
|
||||
type filesOnlyFs struct {
|
||||
@@ -140,525 +84,149 @@ func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
// If a Destination is provided via flag write to disk
|
||||
destination, _ := cmd.Flags().GetString("destination")
|
||||
if destination != "" {
|
||||
sc.renderToDisk = true
|
||||
func init() {
|
||||
initHugoBuilderFlags(serverCmd)
|
||||
|
||||
serverCmd.Flags().IntVarP(&serverPort, "port", "p", 1313, "port on which the server will listen")
|
||||
serverCmd.Flags().StringVarP(&serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
|
||||
serverCmd.Flags().BoolVarP(&serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
|
||||
serverCmd.Flags().BoolVarP(&serverAppend, "appendPort", "", true, "append port to baseURL")
|
||||
serverCmd.Flags().BoolVar(&disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
|
||||
serverCmd.Flags().BoolVar(&renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)")
|
||||
serverCmd.Flags().String("memstats", "", "log memory usage to this file")
|
||||
serverCmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
|
||||
|
||||
serverCmd.RunE = server
|
||||
|
||||
mime.AddExtensionType(".json", "application/json; charset=utf-8")
|
||||
mime.AddExtensionType(".css", "text/css; charset=utf-8")
|
||||
|
||||
}
|
||||
|
||||
func server(cmd *cobra.Command, args []string) error {
|
||||
if err := InitializeConfig(serverCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var serverCfgInit sync.Once
|
||||
|
||||
cfgInit := func(c *commandeer) (rerr error) {
|
||||
c.Set("renderToMemory", !(sc.renderToDisk || sc.renderStaticToDisk))
|
||||
c.Set("renderStaticToDisk", sc.renderStaticToDisk)
|
||||
if cmd.Flags().Changed("navigateToChanged") {
|
||||
c.Set("navigateToChanged", sc.navigateToChanged)
|
||||
}
|
||||
if cmd.Flags().Changed("disableLiveReload") {
|
||||
c.Set("disableLiveReload", sc.disableLiveReload)
|
||||
}
|
||||
if cmd.Flags().Changed("disableFastRender") {
|
||||
c.Set("disableFastRender", sc.disableFastRender)
|
||||
}
|
||||
if cmd.Flags().Changed("disableBrowserError") {
|
||||
c.Set("disableBrowserError", sc.disableBrowserError)
|
||||
}
|
||||
if sc.serverWatch {
|
||||
c.Set("watch", true)
|
||||
}
|
||||
|
||||
// TODO(bep) see issue 9901
|
||||
// cfgInit is called twice, before and after the languages have been initialized.
|
||||
// The servers (below) can not be initialized before we
|
||||
// know if we're configured in a multihost setup.
|
||||
if len(c.languages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We can only do this once.
|
||||
serverCfgInit.Do(func() {
|
||||
c.serverPorts = make([]serverPortListener, 1)
|
||||
|
||||
if c.languages.IsMultihost() {
|
||||
if !sc.serverAppend {
|
||||
rerr = newSystemError("--appendPort=false not supported when in multihost mode")
|
||||
}
|
||||
c.serverPorts = make([]serverPortListener, len(c.languages))
|
||||
}
|
||||
|
||||
currentServerPort := sc.serverPort
|
||||
|
||||
for i := 0; i < len(c.serverPorts); i++ {
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort)))
|
||||
if err == nil {
|
||||
c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
|
||||
} else {
|
||||
if i == 0 && sc.cmd.Flags().Changed("port") {
|
||||
// port set explicitly by user -- he/she probably meant it!
|
||||
rerr = newSystemErrorF("Server startup failed: %s", err)
|
||||
return
|
||||
}
|
||||
c.logger.Println("port", sc.serverPort, "already in use, attempting to use an available port")
|
||||
l, sp, err := helpers.TCPListen()
|
||||
if err != nil {
|
||||
rerr = newSystemError("Unable to find alternative port to use:", err)
|
||||
return
|
||||
}
|
||||
c.serverPorts[i] = serverPortListener{ln: l, p: sp.Port}
|
||||
}
|
||||
|
||||
currentServerPort = c.serverPorts[i].p + 1
|
||||
}
|
||||
})
|
||||
|
||||
if rerr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("port", sc.serverPort)
|
||||
if sc.liveReloadPort != -1 {
|
||||
c.Set("liveReloadPort", sc.liveReloadPort)
|
||||
} else {
|
||||
c.Set("liveReloadPort", c.serverPorts[0].p)
|
||||
}
|
||||
|
||||
isMultiHost := c.languages.IsMultihost()
|
||||
for i, language := range c.languages {
|
||||
var serverPort int
|
||||
if isMultiHost {
|
||||
serverPort = c.serverPorts[i].p
|
||||
} else {
|
||||
serverPort = c.serverPorts[0].p
|
||||
}
|
||||
|
||||
baseURL, err := sc.fixURL(language, sc.baseURL, serverPort)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if isMultiHost {
|
||||
language.Set("baseURL", baseURL)
|
||||
}
|
||||
if i == 0 {
|
||||
c.Set("baseURL", baseURL)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
if flagChanged(cmd.Flags(), "disableLiveReload") {
|
||||
viper.Set("disableLiveReload", disableLiveReload)
|
||||
}
|
||||
|
||||
if serverWatch {
|
||||
viper.Set("watch", true)
|
||||
}
|
||||
|
||||
if viper.GetBool("watch") {
|
||||
serverWatch = true
|
||||
watchConfig()
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(serverPort)))
|
||||
if err == nil {
|
||||
l.Close()
|
||||
} else {
|
||||
if flagChanged(serverCmd.Flags(), "port") {
|
||||
// port set explicitly by user -- he/she probably meant it!
|
||||
return newSystemErrorF("Server startup failed: %s", err)
|
||||
}
|
||||
jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
|
||||
sp, err := helpers.FindAvailablePort()
|
||||
if err != nil {
|
||||
return newSystemError("Unable to find alternative port to use:", err)
|
||||
}
|
||||
serverPort = sp.Port
|
||||
}
|
||||
|
||||
viper.Set("port", serverPort)
|
||||
|
||||
baseURL, err = fixURL(baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
viper.Set("baseURL", baseURL)
|
||||
|
||||
if err := memStats(); err != nil {
|
||||
jww.WARN.Println("memstats error:", err)
|
||||
jww.ERROR.Println("memstats error:", err)
|
||||
}
|
||||
|
||||
// silence errors in cobra so we can handle them here
|
||||
cmd.SilenceErrors = true
|
||||
|
||||
c, err := initializeConfig(true, true, true, &sc.hugoBuilderCommon, sc, cfgInit)
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
return err
|
||||
// If a Destination is provided via flag write to disk
|
||||
if destination != "" {
|
||||
renderToDisk = true
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
defer c.timeTrack(time.Now(), "Built")
|
||||
err := c.serverBuild()
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
}
|
||||
return err
|
||||
}()
|
||||
if err != nil {
|
||||
// Hugo writes the output to memory instead of the disk
|
||||
if !renderToDisk {
|
||||
hugofs.SetDestination(new(afero.MemMapFs))
|
||||
// Rendering to memoryFS, publish to Root regardless of publishDir.
|
||||
viper.Set("publishDir", "/")
|
||||
}
|
||||
|
||||
if err := build(serverWatch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch runs its own server as part of the routine
|
||||
if sc.serverWatch {
|
||||
if serverWatch {
|
||||
watchDirs := getDirList()
|
||||
baseWatchDir := viper.GetString("workingDir")
|
||||
for i, dir := range watchDirs {
|
||||
watchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir)
|
||||
}
|
||||
|
||||
rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(watchDirs)), ",")
|
||||
|
||||
jww.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
|
||||
err := NewWatcher(serverPort)
|
||||
|
||||
watchDirs, err := c.getDirList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
|
||||
|
||||
for _, group := range watchGroups {
|
||||
jww.FEEDBACK.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
watcher, err := c.newWatcher(sc.poll, watchDirs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer watcher.Close()
|
||||
|
||||
}
|
||||
|
||||
return c.serve(sc)
|
||||
serve(serverPort)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRootWatchDirsStr(baseDir string, watchDirs []string) string {
|
||||
relWatchDirs := make([]string, len(watchDirs))
|
||||
for i, dir := range watchDirs {
|
||||
relWatchDirs[i], _ = paths.GetRelativePath(dir, baseDir)
|
||||
func serve(port int) {
|
||||
if renderToDisk {
|
||||
jww.FEEDBACK.Println("Serving pages from " + helpers.AbsPathify(viper.GetString("publishDir")))
|
||||
} else {
|
||||
jww.FEEDBACK.Println("Serving pages from memory")
|
||||
}
|
||||
|
||||
return strings.Join(helpers.UniqueStringsSorted(helpers.ExtractRootPaths(relWatchDirs)), ",")
|
||||
}
|
||||
|
||||
type fileServer struct {
|
||||
baseURLs []string
|
||||
roots []string
|
||||
errorTemplate func(err any) (io.Reader, error)
|
||||
c *commandeer
|
||||
s *serverCmd
|
||||
}
|
||||
|
||||
func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request {
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
r2.URL = new(url.URL)
|
||||
*r2.URL = *r.URL
|
||||
r2.URL.Path = toPath
|
||||
r2.Header.Set("X-Rewrite-Original-URI", r.URL.RequestURI())
|
||||
|
||||
return r2
|
||||
}
|
||||
|
||||
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string, string, error) {
|
||||
baseURL := f.baseURLs[i]
|
||||
root := f.roots[i]
|
||||
port := f.c.serverPorts[i].p
|
||||
listener := f.c.serverPorts[i].ln
|
||||
|
||||
// For logging only.
|
||||
// TODO(bep) consolidate.
|
||||
publishDir := f.c.Cfg.GetString("publishDir")
|
||||
publishDirStatic := f.c.Cfg.GetString("publishDirStatic")
|
||||
workingDir := f.c.Cfg.GetString("workingDir")
|
||||
|
||||
if root != "" {
|
||||
publishDir = filepath.Join(publishDir, root)
|
||||
publishDirStatic = filepath.Join(publishDirStatic, root)
|
||||
}
|
||||
absPublishDir := paths.AbsPathify(workingDir, publishDir)
|
||||
absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
|
||||
|
||||
jww.FEEDBACK.Printf("Environment: %q", f.c.hugo().Deps.Site.Hugo().Environment)
|
||||
|
||||
if i == 0 {
|
||||
if f.s.renderToDisk {
|
||||
jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
|
||||
} else if f.s.renderStaticToDisk {
|
||||
jww.FEEDBACK.Println("Serving pages from memory and static files from " + absPublishDirStatic)
|
||||
} else {
|
||||
jww.FEEDBACK.Println("Serving pages from memory")
|
||||
}
|
||||
}
|
||||
|
||||
httpFs := afero.NewHttpFs(f.c.publishDirServerFs)
|
||||
fs := filesOnlyFs{httpFs.Dir(path.Join("/", root))}
|
||||
|
||||
if i == 0 && f.c.fastRenderMode {
|
||||
jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
|
||||
}
|
||||
httpFs := afero.NewHttpFs(hugofs.Destination())
|
||||
fs := filesOnlyFs{httpFs.Dir(helpers.AbsPathify(viper.GetString("publishDir")))}
|
||||
fileserver := http.FileServer(fs)
|
||||
|
||||
// We're only interested in the path
|
||||
u, err := url.Parse(baseURL)
|
||||
u, err := url.Parse(viper.GetString("baseURL"))
|
||||
if err != nil {
|
||||
return nil, nil, "", "", fmt.Errorf("Invalid baseURL: %w", err)
|
||||
jww.ERROR.Fatalf("Invalid baseURL: %s", err)
|
||||
}
|
||||
|
||||
decorate := func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if f.c.showErrorInBrowser {
|
||||
// First check the error state
|
||||
err := f.c.getErrorWithContext()
|
||||
if err != nil {
|
||||
f.c.wasError = true
|
||||
w.WriteHeader(500)
|
||||
r, err := f.errorTemplate(err)
|
||||
if err != nil {
|
||||
f.c.logger.Errorln(err)
|
||||
}
|
||||
|
||||
port = 1313
|
||||
if !f.c.paused {
|
||||
port = f.c.Cfg.GetInt("liveReloadPort")
|
||||
}
|
||||
lr := *u
|
||||
lr.Host = fmt.Sprintf("%s:%d", lr.Hostname(), port)
|
||||
fmt.Fprint(w, injectLiveReloadScript(r, lr))
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if f.s.noHTTPCache {
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
}
|
||||
|
||||
// Ignore any query params for the operations below.
|
||||
requestURI := strings.TrimSuffix(r.RequestURI, "?"+r.URL.RawQuery)
|
||||
|
||||
for _, header := range f.c.serverConfig.MatchHeaders(requestURI) {
|
||||
w.Header().Set(header.Key, header.Value)
|
||||
}
|
||||
|
||||
if redirect := f.c.serverConfig.MatchRedirect(requestURI); !redirect.IsZero() {
|
||||
doRedirect := true
|
||||
// This matches Netlify's behaviour and is needed for SPA behaviour.
|
||||
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
|
||||
if !redirect.Force {
|
||||
path := filepath.Clean(strings.TrimPrefix(requestURI, u.Path))
|
||||
fi, err := f.c.hugo().BaseFs.PublishFs.Stat(path)
|
||||
if err == nil {
|
||||
if fi.IsDir() {
|
||||
// There will be overlapping directories, so we
|
||||
// need to check for a file.
|
||||
_, err = f.c.hugo().BaseFs.PublishFs.Stat(filepath.Join(path, "index.html"))
|
||||
doRedirect = err != nil
|
||||
} else {
|
||||
doRedirect = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if doRedirect {
|
||||
if redirect.Status == 200 {
|
||||
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil {
|
||||
requestURI = redirect.To
|
||||
r = r2
|
||||
}
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "")
|
||||
http.Redirect(w, r, redirect.To, redirect.Status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if f.c.fastRenderMode && f.c.buildErr == nil {
|
||||
if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") {
|
||||
if !f.c.visitedURLs.Contains(requestURI) {
|
||||
// If not already on stack, re-render that single page.
|
||||
if err := f.c.partialReRender(requestURI); err != nil {
|
||||
f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", requestURI))
|
||||
if f.c.showErrorInBrowser {
|
||||
http.Redirect(w, r, requestURI, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.c.visitedURLs.Add(requestURI)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
fileserver := decorate(http.FileServer(fs))
|
||||
mu := http.NewServeMux()
|
||||
if u.Path == "" || u.Path == "/" {
|
||||
mu.Handle("/", fileserver)
|
||||
http.Handle("/", fileserver)
|
||||
} else {
|
||||
mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
|
||||
}
|
||||
|
||||
endpoint := net.JoinHostPort(f.s.serverInterface, strconv.Itoa(port))
|
||||
|
||||
return mu, listener, u.String(), endpoint, nil
|
||||
}
|
||||
|
||||
var (
|
||||
logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
|
||||
logDuplicateTemplateExecuteRe = regexp.MustCompile(`: template: .*?:\d+:\d+: executing ".*?"`)
|
||||
logDuplicateTemplateParseRe = regexp.MustCompile(`: template: .*?:\d+:\d*`)
|
||||
)
|
||||
|
||||
func removeErrorPrefixFromLog(content string) string {
|
||||
return logErrorRe.ReplaceAllLiteralString(content, "")
|
||||
}
|
||||
|
||||
var logReplacer = strings.NewReplacer(
|
||||
"can't", "can’t", // Chroma lexer does'nt do well with "can't"
|
||||
"*hugolib.pageState", "page.Page", // Page is the public interface.
|
||||
"Rebuild failed:", "",
|
||||
)
|
||||
|
||||
func cleanErrorLog(content string) string {
|
||||
content = strings.ReplaceAll(content, "\n", " ")
|
||||
content = logReplacer.Replace(content)
|
||||
content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
|
||||
content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
|
||||
seen := make(map[string]bool)
|
||||
parts := strings.Split(content, ": ")
|
||||
keep := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if seen[part] {
|
||||
continue
|
||||
}
|
||||
seen[part] = true
|
||||
keep = append(keep, part)
|
||||
}
|
||||
return strings.Join(keep, ": ")
|
||||
}
|
||||
|
||||
func (c *commandeer) serve(s *serverCmd) error {
|
||||
isMultiHost := c.hugo().IsMultihost()
|
||||
|
||||
var (
|
||||
baseURLs []string
|
||||
roots []string
|
||||
)
|
||||
|
||||
if isMultiHost {
|
||||
for _, s := range c.hugo().Sites {
|
||||
baseURLs = append(baseURLs, s.BaseURL.String())
|
||||
roots = append(roots, s.Language().Lang)
|
||||
}
|
||||
} else {
|
||||
s := c.hugo().Sites[0]
|
||||
baseURLs = []string{s.BaseURL.String()}
|
||||
roots = []string{""}
|
||||
}
|
||||
|
||||
// Cache it here. The HugoSites object may be unavaialble later on due to intermitent configuration errors.
|
||||
// To allow the en user to change the error template while the server is running, we use
|
||||
// the freshest template we can provide.
|
||||
var (
|
||||
errTempl tpl.Template
|
||||
templHandler tpl.TemplateHandler
|
||||
)
|
||||
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
|
||||
if h == nil {
|
||||
return errTempl, templHandler
|
||||
}
|
||||
templHandler := h.Tmpl()
|
||||
errTempl, found := templHandler.Lookup("_server/error.html")
|
||||
if !found {
|
||||
panic("template server/error.html not found")
|
||||
}
|
||||
return errTempl, templHandler
|
||||
}
|
||||
errTempl, templHandler = getErrorTemplateAndHandler(c.hugo())
|
||||
|
||||
srv := &fileServer{
|
||||
baseURLs: baseURLs,
|
||||
roots: roots,
|
||||
c: c,
|
||||
s: s,
|
||||
errorTemplate: func(ctx any) (io.Reader, error) {
|
||||
// hugoTry does not block, getErrorTemplateAndHandler will fall back
|
||||
// to cached values if nil.
|
||||
templ, handler := getErrorTemplateAndHandler(c.hugoTry())
|
||||
b := &bytes.Buffer{}
|
||||
err := handler.Execute(templ, b, ctx)
|
||||
return b, err
|
||||
},
|
||||
}
|
||||
|
||||
doLiveReload := !c.Cfg.GetBool("disableLiveReload")
|
||||
|
||||
if doLiveReload {
|
||||
livereload.Initialize()
|
||||
}
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
var servers []*http.Server
|
||||
|
||||
wg1, ctx := errgroup.WithContext(context.Background())
|
||||
|
||||
for i := range baseURLs {
|
||||
mu, listener, serverURL, endpoint, err := srv.createEndpoint(i)
|
||||
srv := &http.Server{
|
||||
Addr: endpoint,
|
||||
Handler: mu,
|
||||
}
|
||||
servers = append(servers, srv)
|
||||
|
||||
if doLiveReload {
|
||||
u, err := url.Parse(helpers.SanitizeURL(baseURLs[i]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mu.HandleFunc(u.Path+"/livereload.js", livereload.ServeJS)
|
||||
mu.HandleFunc(u.Path+"/livereload", livereload.Handler)
|
||||
}
|
||||
jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", serverURL, s.serverInterface)
|
||||
wg1.Go(func() error {
|
||||
err = srv.Serve(listener)
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
http.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", u.String(), serverInterface)
|
||||
jww.FEEDBACK.Println("Press Ctrl+C to stop")
|
||||
|
||||
err := func() error {
|
||||
if s.stop != nil {
|
||||
for {
|
||||
select {
|
||||
case <-sigs:
|
||||
return nil
|
||||
case <-s.stop:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
select {
|
||||
case <-sigs:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
endpoint := net.JoinHostPort(serverInterface, strconv.Itoa(port))
|
||||
err = http.ListenAndServe(endpoint, nil)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Error:", err)
|
||||
jww.ERROR.Printf("Error: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if h := c.hugoTry(); h != nil {
|
||||
h.Close()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
wg2, ctx := errgroup.WithContext(ctx)
|
||||
for _, srv := range servers {
|
||||
srv := srv
|
||||
wg2.Go(func() error {
|
||||
return srv.Shutdown(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
err1, err2 := wg1.Wait(), wg2.Wait()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
// fixURL massages the baseURL into a form needed for serving
|
||||
// all pages correctly.
|
||||
func (sc *serverCmd) fixURL(cfg config.Provider, s string, port int) (string, error) {
|
||||
func fixURL(s string) (string, error) {
|
||||
useLocalhost := false
|
||||
if s == "" {
|
||||
s = cfg.GetString("baseURL")
|
||||
s = viper.GetString("baseURL")
|
||||
useLocalhost = true
|
||||
}
|
||||
|
||||
@@ -690,25 +258,23 @@ func (sc *serverCmd) fixURL(cfg config.Provider, s string, port int) (string, er
|
||||
u.Host = "localhost"
|
||||
}
|
||||
|
||||
if sc.serverAppend {
|
||||
if serverAppend {
|
||||
if strings.Contains(u.Host, ":") {
|
||||
u.Host, _, err = net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to split baseURL hostpost: %w", err)
|
||||
return "", fmt.Errorf("Failed to split baseURL hostpost: %s", err)
|
||||
}
|
||||
}
|
||||
u.Host += fmt.Sprintf(":%d", port)
|
||||
u.Host += fmt.Sprintf(":%d", serverPort)
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func memStats() error {
|
||||
b := newCommandsBuilder()
|
||||
sc := b.newServerCmd().getCommand()
|
||||
memstats := sc.Flags().Lookup("memstats").Value.String()
|
||||
memstats := serverCmd.Flags().Lookup("memstats").Value.String()
|
||||
if memstats != "" {
|
||||
interval, err := time.ParseDuration(sc.Flags().Lookup("meminterval").Value.String())
|
||||
interval, err := time.ParseDuration(serverCmd.Flags().Lookup("meminterval").Value.String())
|
||||
if err != nil {
|
||||
interval, _ = time.ParseDuration("100ms")
|
||||
}
|
||||
@@ -723,13 +289,13 @@ func memStats() error {
|
||||
go func() {
|
||||
var stats runtime.MemStats
|
||||
|
||||
start := htime.Now().UnixNano()
|
||||
start := time.Now().UnixNano()
|
||||
|
||||
for {
|
||||
runtime.ReadMemStats(&stats)
|
||||
if fileMemStats != nil {
|
||||
fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n",
|
||||
(htime.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased))
|
||||
(time.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased))
|
||||
time.Sleep(interval)
|
||||
} else {
|
||||
break
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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()
|
||||
}
|
||||
+16
-236
@@ -14,215 +14,14 @@
|
||||
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"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
defer viper.Reset()
|
||||
|
||||
type data struct {
|
||||
TestName string
|
||||
CLIBaseURL string
|
||||
@@ -244,37 +43,18 @@ func TestFixURL(t *testing.T) {
|
||||
{"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)
|
||||
}
|
||||
})
|
||||
for i, test := range tests {
|
||||
viper.Reset()
|
||||
baseURL = test.CLIBaseURL
|
||||
viper.Set("baseURL", test.CfgBaseURL)
|
||||
serverAppend = test.AppendPort
|
||||
serverPort = test.Port
|
||||
result, err := fixURL(baseURL)
|
||||
if err != nil {
|
||||
t.Errorf("Test #%d %s: unexpected error %s", i, test.TestName, err)
|
||||
}
|
||||
if result != test.Result {
|
||||
t.Errorf("Test #%d %s: expected %q, got %q", i, test.TestName, test.Result, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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") != ""
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
// 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,156 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/hugo/hugofs"
|
||||
"github.com/spf13/hugo/parser"
|
||||
)
|
||||
|
||||
var undraftCmd = &cobra.Command{
|
||||
Use: "undraft path/to/content",
|
||||
Short: "Undraft changes the content's draft status from 'True' to 'False'",
|
||||
Long: `Undraft changes the content's draft status from 'True' to 'False'
|
||||
and updates the date to the current date and time.
|
||||
If the content's draft status is 'False', nothing is done.`,
|
||||
RunE: Undraft,
|
||||
}
|
||||
|
||||
// Undraft publishes the specified content by setting its draft status
|
||||
// to false and setting its publish date to now. If the specified content is
|
||||
// not a draft, it will log an error.
|
||||
func Undraft(cmd *cobra.Command, args []string) error {
|
||||
if err := InitializeConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
return newUserError("a piece of content needs to be specified")
|
||||
}
|
||||
|
||||
location := args[0]
|
||||
// open the file
|
||||
f, err := hugofs.Source().Open(location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the page from file
|
||||
p, err := parser.ReadFrom(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := undraftContent(p)
|
||||
if err != nil {
|
||||
return newSystemErrorF("an error occurred while undrafting %q: %s", location, err)
|
||||
}
|
||||
|
||||
f, err = hugofs.Source().OpenFile(location, os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return newSystemErrorF("%q not be undrafted due to error opening file to save changes: %q\n", location, err)
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = w.WriteTo(f)
|
||||
if err != nil {
|
||||
return newSystemErrorF("%q not be undrafted due to save error: %q\n", location, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// undraftContent: if the content is a draft, change its draft status to
|
||||
// 'false' and set the date to time.Now(). If the draft status is already
|
||||
// 'false', don't do anything.
|
||||
func undraftContent(p parser.Page) (bytes.Buffer, error) {
|
||||
var buff bytes.Buffer
|
||||
// get the metadata; easiest way to see if it's a draft
|
||||
meta, err := p.Metadata()
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
// since the metadata was obtainable, we can also get the key/value separator for
|
||||
// Front Matter
|
||||
fm := p.FrontMatter()
|
||||
if fm == nil {
|
||||
return buff, errors.New("Front Matter was found, nothing was finalized")
|
||||
}
|
||||
|
||||
var isDraft, gotDate bool
|
||||
var date string
|
||||
L:
|
||||
for k, v := range meta.(map[string]interface{}) {
|
||||
switch k {
|
||||
case "draft":
|
||||
if !v.(bool) {
|
||||
return buff, errors.New("not a Draft: nothing was done")
|
||||
}
|
||||
isDraft = true
|
||||
if gotDate {
|
||||
break L
|
||||
}
|
||||
case "date":
|
||||
date = v.(string) // capture the value to make replacement easier
|
||||
gotDate = true
|
||||
if isDraft {
|
||||
break L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if draft wasn't found in FrontMatter, it isn't a draft.
|
||||
if !isDraft {
|
||||
return buff, errors.New("not a Draft: nothing was done")
|
||||
}
|
||||
|
||||
// get the front matter as bytes and split it into lines
|
||||
var lineEnding []byte
|
||||
fmLines := bytes.Split(fm, []byte("\n"))
|
||||
if len(fmLines) == 1 { // if the result is only 1 element, try to split on dos line endings
|
||||
fmLines = bytes.Split(fm, []byte("\r\n"))
|
||||
if len(fmLines) == 1 {
|
||||
return buff, errors.New("unable to split FrontMatter into lines")
|
||||
}
|
||||
lineEnding = append(lineEnding, []byte("\r\n")...)
|
||||
} else {
|
||||
lineEnding = append(lineEnding, []byte("\n")...)
|
||||
}
|
||||
|
||||
// Write the front matter lines to the buffer, replacing as necessary
|
||||
for _, v := range fmLines {
|
||||
pos := bytes.Index(v, []byte("draft"))
|
||||
if pos != -1 {
|
||||
v = bytes.Replace(v, []byte("true"), []byte("false"), 1)
|
||||
goto write
|
||||
}
|
||||
pos = bytes.Index(v, []byte("date"))
|
||||
if pos != -1 { // if date field wasn't found, add it
|
||||
v = bytes.Replace(v, []byte(date), []byte(time.Now().Format(time.RFC3339)), 1)
|
||||
}
|
||||
write:
|
||||
buff.Write(v)
|
||||
buff.Write(lineEnding)
|
||||
}
|
||||
|
||||
// append the actual content
|
||||
buff.Write(p.Content())
|
||||
|
||||
return buff, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
// TODO Support Mac Encoding (\r)
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/hugo/parser"
|
||||
)
|
||||
|
||||
var (
|
||||
jsonFM = "{\n \"date\": \"12-04-06\",\n \"title\": \"test json\"\n}"
|
||||
jsonDraftFM = "{\n \"draft\": true,\n \"date\": \"12-04-06\",\n \"title\":\"test json\"\n}"
|
||||
tomlFM = "+++\n date= \"12-04-06\"\n title= \"test toml\"\n+++"
|
||||
tomlDraftFM = "+++\n draft= true\n date= \"12-04-06\"\n title=\"test toml\"\n+++"
|
||||
yamlFM = "---\n date: \"12-04-06\"\n title: \"test yaml\"\n---"
|
||||
yamlDraftFM = "---\n draft: true\n date: \"12-04-06\"\n title: \"test yaml\"\n---"
|
||||
)
|
||||
|
||||
func TestUndraftContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
fm string
|
||||
expectedErr string
|
||||
}{
|
||||
{jsonFM, "not a Draft: nothing was done"},
|
||||
{jsonDraftFM, ""},
|
||||
{tomlFM, "not a Draft: nothing was done"},
|
||||
{tomlDraftFM, ""},
|
||||
{yamlFM, "not a Draft: nothing was done"},
|
||||
{yamlDraftFM, ""},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
r := bytes.NewReader([]byte(test.fm))
|
||||
p, _ := parser.ReadFrom(r)
|
||||
res, err := undraftContent(p)
|
||||
if test.expectedErr != "" {
|
||||
if err == nil {
|
||||
t.Error("Expected error, got none")
|
||||
continue
|
||||
}
|
||||
if err.Error() != test.expectedErr {
|
||||
t.Errorf("Expected %q, got %q", test.expectedErr, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
r = bytes.NewReader(res.Bytes())
|
||||
p, _ = parser.ReadFrom(r)
|
||||
meta, err := p.Metadata()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %q", err)
|
||||
continue
|
||||
}
|
||||
for k, v := range meta.(map[string]interface{}) {
|
||||
if k == "draft" {
|
||||
if v.(bool) {
|
||||
t.Errorf("Expected %q to be \"false\", got \"true\"", k)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if k == "date" {
|
||||
if !strings.HasPrefix(v.(string), time.Now().Format("2006-01-02")) {
|
||||
t.Errorf("Expected %v to start with %v", v.(string), time.Now().Format("2006-01-02"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
-20
@@ -14,31 +14,66 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/osext"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/spf13/hugo/hugolib"
|
||||
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
|
||||
},
|
||||
}),
|
||||
}
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number of Hugo",
|
||||
Long: `All software has versions. This is Hugo's.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
printHugoVersion()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func printHugoVersion() {
|
||||
jww.FEEDBACK.Println(hugo.BuildVersionString())
|
||||
if hugolib.BuildDate == "" {
|
||||
setBuildDate() // set the build date from executable's mdate
|
||||
} else {
|
||||
formatBuildDate() // format the compile time
|
||||
}
|
||||
if hugolib.CommitHash == "" {
|
||||
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s BuildDate: %s\n", helpers.HugoVersion(), hugolib.BuildDate)
|
||||
} else {
|
||||
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s-%s BuildDate: %s\n", helpers.HugoVersion(), strings.ToUpper(hugolib.CommitHash), hugolib.BuildDate)
|
||||
}
|
||||
}
|
||||
|
||||
// setBuildDate checks the ModTime of the Hugo executable and returns it as a
|
||||
// formatted string. This assumes that the executable name is Hugo, if it does
|
||||
// not exist, an empty string will be returned. This is only called if the
|
||||
// hugolib.BuildDate wasn't set during compile time.
|
||||
//
|
||||
// osext is used for cross-platform.
|
||||
func setBuildDate() {
|
||||
fname, _ := osext.Executable()
|
||||
dir, err := filepath.Abs(filepath.Dir(fname))
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
return
|
||||
}
|
||||
fi, err := os.Lstat(filepath.Join(dir, filepath.Base(fname)))
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
return
|
||||
}
|
||||
t := fi.ModTime()
|
||||
hugolib.BuildDate = t.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// formatBuildDate formats the hugolib.BuildDate according to the value in
|
||||
// .Params.DateFormat, if it's set.
|
||||
func formatBuildDate() {
|
||||
t, _ := time.Parse("2006-01-02T15:04:05-0700", hugolib.BuildDate)
|
||||
hugolib.BuildDate = t.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collections
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Append appends from to a slice to and returns the resulting slice.
|
||||
// If length of from is one and the only element is a slice of same type as to,
|
||||
// it will be appended.
|
||||
func Append(to any, from ...any) (any, error) {
|
||||
tov, toIsNil := indirect(reflect.ValueOf(to))
|
||||
|
||||
toIsNil = toIsNil || to == nil
|
||||
var tot reflect.Type
|
||||
|
||||
if !toIsNil {
|
||||
if tov.Kind() != reflect.Slice {
|
||||
return nil, fmt.Errorf("expected a slice, got %T", to)
|
||||
}
|
||||
|
||||
tot = tov.Type().Elem()
|
||||
toIsNil = tov.Len() == 0
|
||||
|
||||
if len(from) == 1 {
|
||||
fromv := reflect.ValueOf(from[0])
|
||||
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
|
||||
} else if !fromt.AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
return appendToInterfaceSliceFromValues(tov, fromv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if toIsNil {
|
||||
return Slice(from...), nil
|
||||
}
|
||||
|
||||
for _, f := range from {
|
||||
fv := reflect.ValueOf(f)
|
||||
if !fv.Type().AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
tov, _ := indirect(reflect.ValueOf(to))
|
||||
return appendToInterfaceSlice(tov, from...)
|
||||
}
|
||||
tov = reflect.Append(tov, fv)
|
||||
}
|
||||
|
||||
return tov.Interface(), nil
|
||||
}
|
||||
|
||||
func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, error) {
|
||||
var tos []any
|
||||
|
||||
for _, slice := range []reflect.Value{slice1, slice2} {
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
tos = append(tos, slice.Index(i).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
return tos, nil
|
||||
}
|
||||
|
||||
func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
|
||||
var tos []any
|
||||
|
||||
for i := 0; i < tov.Len(); i++ {
|
||||
tos = append(tos, tov.Index(i).Interface())
|
||||
}
|
||||
|
||||
tos = append(tos, from...)
|
||||
|
||||
return tos, nil
|
||||
}
|
||||
|
||||
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
|
||||
// TODO(bep) consolidate
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collections
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
start any
|
||||
addend []any
|
||||
expected any
|
||||
}{
|
||||
{[]string{"a", "b"}, []any{"c"}, []string{"a", "b", "c"}},
|
||||
{[]string{"a", "b"}, []any{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}},
|
||||
{[]string{"a", "b"}, []any{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}},
|
||||
{[]string{"a"}, []any{"b", template.HTML("c")}, []any{"a", "b", template.HTML("c")}},
|
||||
{nil, []any{"a", "b"}, []string{"a", "b"}},
|
||||
{nil, []any{nil}, []any{nil}},
|
||||
{[]any{}, []any{[]string{"c", "d", "e"}}, []string{"c", "d", "e"}},
|
||||
{
|
||||
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]any{&tstSlicer{"c"}},
|
||||
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}},
|
||||
},
|
||||
{
|
||||
&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]any{&tstSlicer{"c"}},
|
||||
tstSlicers{
|
||||
&tstSlicer{"a"},
|
||||
&tstSlicer{"b"},
|
||||
&tstSlicer{"c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}},
|
||||
[]any{&tstSlicerIn1{"c"}},
|
||||
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}},
|
||||
},
|
||||
//https://github.com/gohugoio/hugo/issues/5361
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
[]any{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
[]any{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
},
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
[]any{&tstSlicer{"a"}},
|
||||
[]any{"a", "b", &tstSlicer{"a"}},
|
||||
},
|
||||
// Errors
|
||||
{"", []any{[]string{"a", "b"}}, false},
|
||||
// No string concatenation.
|
||||
{
|
||||
"ab",
|
||||
[]any{"c"},
|
||||
false,
|
||||
},
|
||||
} {
|
||||
|
||||
result, err := Append(test.start, test.addend...)
|
||||
|
||||
if b, ok := test.expected.(bool); ok && !b {
|
||||
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
continue
|
||||
}
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(result, qt.DeepEquals, test.expected)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package collections contains common Hugo functionality related to collection
|
||||
// handling.
|
||||
package collections
|
||||
|
||||
// Grouper defines a very generic way to group items by a given key.
|
||||
type Grouper interface {
|
||||
Group(key any, items any) (any, error)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collections
|
||||
|
||||
type Order interface {
|
||||
// Ordinal is a zero-based ordinal that represents the order of an object
|
||||
// in a collection.
|
||||
Ordinal() int
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collections
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Slicer defines a very generic way to create a typed slice. This is used
|
||||
// in collections.Slice template func to get types such as Pages, PageGroups etc.
|
||||
// instead of the less useful []interface{}.
|
||||
type Slicer interface {
|
||||
Slice(items any) (any, error)
|
||||
}
|
||||
|
||||
// Slice returns a slice of all passed arguments.
|
||||
func Slice(args ...any) any {
|
||||
if len(args) == 0 {
|
||||
return args
|
||||
}
|
||||
|
||||
first := args[0]
|
||||
firstType := reflect.TypeOf(first)
|
||||
|
||||
if firstType == nil {
|
||||
return args
|
||||
}
|
||||
|
||||
if g, ok := first.(Slicer); ok {
|
||||
v, err := g.Slice(args)
|
||||
if err == nil {
|
||||
return v
|
||||
}
|
||||
|
||||
// If Slice fails, the items are not of the same type and
|
||||
// []interface{} is the best we can do.
|
||||
return args
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
// This can be a mix of types.
|
||||
for i := 1; i < len(args); i++ {
|
||||
if firstType != reflect.TypeOf(args[i]) {
|
||||
// []interface{} is the best we can do
|
||||
return args
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slice := reflect.MakeSlice(reflect.SliceOf(firstType), len(args), len(args))
|
||||
for i, arg := range args {
|
||||
slice.Index(i).Set(reflect.ValueOf(arg))
|
||||
}
|
||||
return slice.Interface()
|
||||
}
|
||||
|
||||
// StringSliceToInterfaceSlice converts ss to []interface{}.
|
||||
func StringSliceToInterfaceSlice(ss []string) []any {
|
||||
result := make([]any, len(ss))
|
||||
for i, s := range ss {
|
||||
result[i] = s
|
||||
}
|
||||
return result
|
||||
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collections
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Slicer = (*tstSlicer)(nil)
|
||||
_ Slicer = (*tstSlicerIn1)(nil)
|
||||
_ Slicer = (*tstSlicerIn2)(nil)
|
||||
_ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
_ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
)
|
||||
|
||||
type testSlicerInterface interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
type testSlicerInterfaces []testSlicerInterface
|
||||
|
||||
type tstSlicerIn1 struct {
|
||||
TheName string
|
||||
}
|
||||
|
||||
type tstSlicerIn2 struct {
|
||||
TheName string
|
||||
}
|
||||
|
||||
type tstSlicer struct {
|
||||
TheName string
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn1) Slice(in any) (any, error) {
|
||||
items := in.([]any)
|
||||
result := make(testSlicerInterfaces, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
case testSlicerInterface:
|
||||
result[i] = vv
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn2) Slice(in any) (any, error) {
|
||||
items := in.([]any)
|
||||
result := make(testSlicerInterfaces, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
case testSlicerInterface:
|
||||
result[i] = vv
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn1) Name() string {
|
||||
return p.TheName
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn2) Name() string {
|
||||
return p.TheName
|
||||
}
|
||||
|
||||
func (p *tstSlicer) Slice(in any) (any, error) {
|
||||
items := in.([]any)
|
||||
result := make(tstSlicers, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
case *tstSlicer:
|
||||
result[i] = vv
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type tstSlicers []*tstSlicer
|
||||
|
||||
func TestSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
for i, test := range []struct {
|
||||
args []any
|
||||
expected any
|
||||
}{
|
||||
{[]any{"a", "b"}, []string{"a", "b"}},
|
||||
{[]any{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
{[]any{&tstSlicer{"a"}, "b"}, []any{&tstSlicer{"a"}, "b"}},
|
||||
{[]any{}, []any{}},
|
||||
{[]any{nil}, []any{nil}},
|
||||
{[]any{5, "b"}, []any{5, "b"}},
|
||||
{[]any{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}},
|
||||
{[]any{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}, []any{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}},
|
||||
} {
|
||||
errMsg := qt.Commentf("[%d] %v", i, test.args)
|
||||
|
||||
result := Slice(test.args...)
|
||||
|
||||
c.Assert(test.expected, qt.DeepEquals, result, errMsg)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package constants
|
||||
|
||||
// Error 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,168 +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 contains common Hugo errors and error related utilities.
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
)
|
||||
|
||||
// LineMatcher contains the elements used to match an error to a line
|
||||
type LineMatcher struct {
|
||||
Position text.Position
|
||||
Error error
|
||||
|
||||
LineNumber int
|
||||
Offset int
|
||||
Line string
|
||||
}
|
||||
|
||||
// LineMatcherFn is used to match a line with an error.
|
||||
// It returns the column number or 0 if the line was found, but column could not be determinde. Returns -1 if no line match.
|
||||
type LineMatcherFn func(m LineMatcher) int
|
||||
|
||||
// SimpleLineMatcher simply matches by line number.
|
||||
var SimpleLineMatcher = func(m LineMatcher) int {
|
||||
if m.Position.LineNumber == m.LineNumber {
|
||||
// We found the line, but don't know the column.
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// NopLineMatcher is a matcher that always returns 1.
|
||||
// This will effectively give line 1, column 1.
|
||||
var NopLineMatcher = func(m LineMatcher) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// OffsetMatcher is a line matcher that matches by offset.
|
||||
var OffsetMatcher = func(m LineMatcher) int {
|
||||
if m.Offset+len(m.Line) >= m.Position.Offset {
|
||||
// We found the line, but return 0 to signal that we want to determine
|
||||
// the column from the error.
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// ErrorContext contains contextual information about an error. This will
|
||||
// typically be the lines surrounding some problem in a file.
|
||||
type ErrorContext struct {
|
||||
|
||||
// If a match will contain the matched line and up to 2 lines before and after.
|
||||
// Will be empty if no match.
|
||||
Lines []string
|
||||
|
||||
// The position of the error in the Lines above. 0 based.
|
||||
LinesPos int
|
||||
|
||||
// The position of the content in the file. Note that this may be different from the error's position set
|
||||
// in FileError.
|
||||
Position text.Position
|
||||
|
||||
// The lexer to use for syntax highlighting.
|
||||
// https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages
|
||||
ChromaLexer string
|
||||
}
|
||||
|
||||
func chromaLexerFromType(fileType string) string {
|
||||
switch fileType {
|
||||
case "html", "htm":
|
||||
return "go-html-template"
|
||||
}
|
||||
return fileType
|
||||
}
|
||||
|
||||
func extNoDelimiter(filename string) string {
|
||||
return strings.TrimPrefix(filepath.Ext(filename), ".")
|
||||
}
|
||||
|
||||
func chromaLexerFromFilename(filename string) string {
|
||||
if strings.Contains(filename, "layouts") {
|
||||
return "go-html-template"
|
||||
}
|
||||
|
||||
ext := extNoDelimiter(filename)
|
||||
return chromaLexerFromType(ext)
|
||||
}
|
||||
|
||||
func locateErrorInString(src string, matcher LineMatcherFn) *ErrorContext {
|
||||
return locateError(strings.NewReader(src), &fileError{}, matcher)
|
||||
}
|
||||
|
||||
func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext {
|
||||
if le == nil {
|
||||
panic("must provide an error")
|
||||
}
|
||||
|
||||
ectx := &ErrorContext{LinesPos: -1, Position: text.Position{Offset: -1}}
|
||||
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return ectx
|
||||
}
|
||||
|
||||
lines := strings.Split(string(b), "\n")
|
||||
|
||||
lineNo := 0
|
||||
posBytes := 0
|
||||
|
||||
for li, line := range lines {
|
||||
lineNo = li + 1
|
||||
m := LineMatcher{
|
||||
Position: le.Position(),
|
||||
Error: le,
|
||||
LineNumber: lineNo,
|
||||
Offset: posBytes,
|
||||
Line: line,
|
||||
}
|
||||
v := matches(m)
|
||||
if ectx.LinesPos == -1 && v != -1 {
|
||||
ectx.Position.LineNumber = lineNo
|
||||
ectx.Position.ColumnNumber = v
|
||||
break
|
||||
}
|
||||
|
||||
posBytes += len(line)
|
||||
}
|
||||
|
||||
if ectx.Position.LineNumber > 0 {
|
||||
low := ectx.Position.LineNumber - 3
|
||||
if low < 0 {
|
||||
low = 0
|
||||
}
|
||||
|
||||
if ectx.Position.LineNumber > 2 {
|
||||
ectx.LinesPos = 2
|
||||
} else {
|
||||
ectx.LinesPos = ectx.Position.LineNumber - 1
|
||||
}
|
||||
|
||||
high := ectx.Position.LineNumber + 2
|
||||
if high > len(lines) {
|
||||
high = len(lines)
|
||||
}
|
||||
|
||||
ectx.Lines = lines[low:high]
|
||||
|
||||
}
|
||||
|
||||
return ectx
|
||||
}
|
||||
@@ -1,152 +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 contains common Hugo errors and error related utilities.
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestErrorLocator(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
lineMatcher := func(m LineMatcher) int {
|
||||
if strings.Contains(m.Line, "THEONE") {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
lines := `LINE 1
|
||||
LINE 2
|
||||
LINE 3
|
||||
LINE 4
|
||||
This is THEONE
|
||||
LINE 6
|
||||
LINE 7
|
||||
LINE 8
|
||||
`
|
||||
|
||||
location := locateErrorInString(lines, lineMatcher)
|
||||
pos := location.Position
|
||||
c.Assert(location.Lines, qt.DeepEquals, []string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"})
|
||||
|
||||
c.Assert(pos.LineNumber, qt.Equals, 5)
|
||||
c.Assert(location.LinesPos, qt.Equals, 2)
|
||||
|
||||
locate := func(s string, m LineMatcherFn) *ErrorContext {
|
||||
ctx := locateErrorInString(s, m)
|
||||
return ctx
|
||||
}
|
||||
|
||||
c.Assert(locate(`This is THEONE`, lineMatcher).Lines, qt.DeepEquals, []string{"This is THEONE"})
|
||||
|
||||
location = locateErrorInString(`L1
|
||||
This is THEONE
|
||||
L2
|
||||
`, lineMatcher)
|
||||
pos = location.Position
|
||||
c.Assert(pos.LineNumber, qt.Equals, 2)
|
||||
c.Assert(location.LinesPos, qt.Equals, 1)
|
||||
c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "This is THEONE", "L2", ""})
|
||||
|
||||
location = locate(`This is THEONE
|
||||
L2
|
||||
`, lineMatcher)
|
||||
c.Assert(location.LinesPos, qt.Equals, 0)
|
||||
c.Assert(location.Lines, qt.DeepEquals, []string{"This is THEONE", "L2", ""})
|
||||
|
||||
location = locate(`L1
|
||||
This THEONE
|
||||
`, lineMatcher)
|
||||
c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "This THEONE", ""})
|
||||
c.Assert(location.LinesPos, qt.Equals, 1)
|
||||
|
||||
location = locate(`L1
|
||||
L2
|
||||
This THEONE
|
||||
`, lineMatcher)
|
||||
c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "L2", "This THEONE", ""})
|
||||
c.Assert(location.LinesPos, qt.Equals, 2)
|
||||
|
||||
location = locateErrorInString("NO MATCH", lineMatcher)
|
||||
pos = location.Position
|
||||
c.Assert(pos.LineNumber, qt.Equals, 0)
|
||||
c.Assert(location.LinesPos, qt.Equals, -1)
|
||||
c.Assert(len(location.Lines), qt.Equals, 0)
|
||||
|
||||
lineMatcher = func(m LineMatcher) int {
|
||||
if m.LineNumber == 6 {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
location = locateErrorInString(`A
|
||||
B
|
||||
C
|
||||
D
|
||||
E
|
||||
F
|
||||
G
|
||||
H
|
||||
I
|
||||
J`, lineMatcher)
|
||||
pos = location.Position
|
||||
|
||||
c.Assert(location.Lines, qt.DeepEquals, []string{"D", "E", "F", "G", "H"})
|
||||
c.Assert(pos.LineNumber, qt.Equals, 6)
|
||||
c.Assert(location.LinesPos, qt.Equals, 2)
|
||||
|
||||
// Test match EOF
|
||||
lineMatcher = func(m LineMatcher) int {
|
||||
if m.LineNumber == 4 {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
location = locateErrorInString(`A
|
||||
B
|
||||
C
|
||||
`, lineMatcher)
|
||||
|
||||
pos = location.Position
|
||||
|
||||
c.Assert(location.Lines, qt.DeepEquals, []string{"B", "C", ""})
|
||||
c.Assert(pos.LineNumber, qt.Equals, 4)
|
||||
c.Assert(location.LinesPos, qt.Equals, 2)
|
||||
|
||||
offsetMatcher := func(m LineMatcher) int {
|
||||
if m.Offset == 1 {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
location = locateErrorInString(`A
|
||||
B
|
||||
C
|
||||
D
|
||||
E`, offsetMatcher)
|
||||
|
||||
pos = location.Position
|
||||
|
||||
c.Assert(location.Lines, qt.DeepEquals, []string{"A", "B", "C", "D"})
|
||||
c.Assert(pos.LineNumber, qt.Equals, 2)
|
||||
c.Assert(location.LinesPos, qt.Equals, 1)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package herrors contains common Hugo errors and error related utilities.
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// PrintStackTrace prints the current stacktrace to w.
|
||||
func PrintStackTrace(w io.Writer) {
|
||||
buf := make([]byte, 1<<16)
|
||||
runtime.Stack(buf, true)
|
||||
fmt.Fprintf(w, "%s", buf)
|
||||
}
|
||||
|
||||
// ErrorSender is a, typically, non-blocking error handler.
|
||||
type ErrorSender interface {
|
||||
SendError(err error)
|
||||
}
|
||||
|
||||
// Recover is a helper function that can be used to capture panics.
|
||||
// Put this at the top of a method/function that crashes in a template:
|
||||
// defer herrors.Recover()
|
||||
func Recover(args ...any) {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("ERR:", r)
|
||||
args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
|
||||
fmt.Println(args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the current goroutine id. Used only for debugging.
|
||||
func GetGID() uint64 {
|
||||
b := make([]byte, 64)
|
||||
b = b[:runtime.Stack(b, false)]
|
||||
b = bytes.TrimPrefix(b, []byte("goroutine "))
|
||||
b = b[:bytes.IndexByte(b, ' ')]
|
||||
n, _ := strconv.ParseUint(string(b), 10, 64)
|
||||
return n
|
||||
}
|
||||
|
||||
// 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 = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
|
||||
|
||||
// Must panics if err != nil.
|
||||
func Must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,394 +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 lfmtaw or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bep/godartsass"
|
||||
"github.com/bep/golibsass/libsass/libsasserrors"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/tdewolff/parse/v2"
|
||||
|
||||
"errors"
|
||||
)
|
||||
|
||||
// FileError represents an error when handling a file: Parsing a config file,
|
||||
// execute a template etc.
|
||||
type FileError interface {
|
||||
error
|
||||
|
||||
// ErroContext holds some context information about the error.
|
||||
ErrorContext() *ErrorContext
|
||||
|
||||
text.Positioner
|
||||
|
||||
// UpdatePosition updates the position of the error.
|
||||
UpdatePosition(pos text.Position) FileError
|
||||
|
||||
// UpdateContent updates the error with a new ErrorContext from the content of the file.
|
||||
UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError
|
||||
}
|
||||
|
||||
// Unwrapper can unwrap errors created with fmt.Errorf.
|
||||
type Unwrapper interface {
|
||||
Unwrap() error
|
||||
}
|
||||
|
||||
var (
|
||||
_ FileError = (*fileError)(nil)
|
||||
_ Unwrapper = (*fileError)(nil)
|
||||
)
|
||||
|
||||
func (fe *fileError) UpdatePosition(pos text.Position) FileError {
|
||||
oldFilename := fe.Position().Filename
|
||||
if pos.Filename != "" && fe.fileType == "" {
|
||||
_, fe.fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename))
|
||||
}
|
||||
if pos.Filename == "" {
|
||||
pos.Filename = oldFilename
|
||||
}
|
||||
fe.position = pos
|
||||
return fe
|
||||
}
|
||||
|
||||
func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError {
|
||||
if linematcher == nil {
|
||||
linematcher = SimpleLineMatcher
|
||||
}
|
||||
|
||||
var (
|
||||
posle = fe.position
|
||||
ectx *ErrorContext
|
||||
)
|
||||
|
||||
if posle.LineNumber <= 1 && posle.Offset > 0 {
|
||||
// Try to locate the line number from the content if offset is set.
|
||||
ectx = locateError(r, fe, func(m LineMatcher) int {
|
||||
if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) {
|
||||
lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber
|
||||
m.Position = text.Position{LineNumber: lno}
|
||||
return linematcher(m)
|
||||
}
|
||||
return -1
|
||||
})
|
||||
} else {
|
||||
ectx = locateError(r, fe, linematcher)
|
||||
}
|
||||
|
||||
if ectx.ChromaLexer == "" {
|
||||
if fe.fileType != "" {
|
||||
ectx.ChromaLexer = chromaLexerFromType(fe.fileType)
|
||||
} else {
|
||||
ectx.ChromaLexer = chromaLexerFromFilename(fe.Position().Filename)
|
||||
}
|
||||
}
|
||||
|
||||
fe.errorContext = ectx
|
||||
|
||||
if ectx.Position.LineNumber > 0 {
|
||||
fe.position.LineNumber = ectx.Position.LineNumber
|
||||
}
|
||||
|
||||
if ectx.Position.ColumnNumber > 0 {
|
||||
fe.position.ColumnNumber = ectx.Position.ColumnNumber
|
||||
}
|
||||
|
||||
return fe
|
||||
|
||||
}
|
||||
|
||||
type fileError struct {
|
||||
position text.Position
|
||||
errorContext *ErrorContext
|
||||
|
||||
fileType string
|
||||
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e *fileError) ErrorContext() *ErrorContext {
|
||||
return e.errorContext
|
||||
}
|
||||
|
||||
// Position returns the text position of this error.
|
||||
func (e fileError) Position() text.Position {
|
||||
return e.position
|
||||
}
|
||||
|
||||
func (e *fileError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.position, e.causeString())
|
||||
}
|
||||
|
||||
func (e *fileError) causeString() string {
|
||||
if e.cause == nil {
|
||||
return ""
|
||||
}
|
||||
switch v := e.cause.(type) {
|
||||
// Avoid repeating the file info in the error message.
|
||||
case godartsass.SassError:
|
||||
return v.Message
|
||||
case libsasserrors.Error:
|
||||
return v.Message
|
||||
default:
|
||||
return v.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *fileError) Unwrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// NewFileError creates a new FileError that wraps err.
|
||||
// It will try to extract the filename and line number from err.
|
||||
func NewFileError(err error) FileError {
|
||||
// Filetype is used to determine the Chroma lexer to use.
|
||||
fileType, pos := extractFileTypePos(err)
|
||||
return &fileError{cause: err, fileType: fileType, position: pos}
|
||||
}
|
||||
|
||||
// NewFileErrorFromName creates a new FileError that wraps err.
|
||||
// The value for name should identify the file, the best
|
||||
// being the full filename to the file on disk.
|
||||
func NewFileErrorFromName(err error, name string) FileError {
|
||||
// Filetype is used to determine the Chroma lexer to use.
|
||||
fileType, pos := extractFileTypePos(err)
|
||||
pos.Filename = name
|
||||
if fileType == "" {
|
||||
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
|
||||
}
|
||||
|
||||
return &fileError{cause: err, fileType: fileType, position: pos}
|
||||
|
||||
}
|
||||
|
||||
// NewFileErrorFromPos will use the filename and line number from pos to create a new FileError, wrapping err.
|
||||
func NewFileErrorFromPos(err error, pos text.Position) FileError {
|
||||
// Filetype is used to determine the Chroma lexer to use.
|
||||
fileType, _ := extractFileTypePos(err)
|
||||
if fileType == "" {
|
||||
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename))
|
||||
}
|
||||
return &fileError{cause: err, fileType: fileType, position: pos}
|
||||
|
||||
}
|
||||
|
||||
func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError {
|
||||
fe := NewFileError(err)
|
||||
pos := fe.Position()
|
||||
if pos.Filename == "" {
|
||||
return fe
|
||||
}
|
||||
|
||||
f, realFilename, err2 := openFile(pos.Filename, fs)
|
||||
if err2 != nil {
|
||||
return fe
|
||||
}
|
||||
|
||||
pos.Filename = realFilename
|
||||
defer f.Close()
|
||||
return fe.UpdateContent(f, linematcher)
|
||||
}
|
||||
|
||||
func NewFileErrorFromFileInPos(err error, pos text.Position, fs afero.Fs, linematcher LineMatcherFn) FileError {
|
||||
if err == nil {
|
||||
panic("err is nil")
|
||||
}
|
||||
f, realFilename, err2 := openFile(pos.Filename, fs)
|
||||
if err2 != nil {
|
||||
return NewFileErrorFromPos(err, pos)
|
||||
}
|
||||
pos.Filename = realFilename
|
||||
defer f.Close()
|
||||
return NewFileErrorFromPos(err, pos).UpdateContent(f, linematcher)
|
||||
}
|
||||
|
||||
// NewFileErrorFromFile is a convenience method to create a new FileError from a file.
|
||||
func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher LineMatcherFn) FileError {
|
||||
if err == nil {
|
||||
panic("err is nil")
|
||||
}
|
||||
f, realFilename, err2 := openFile(filename, fs)
|
||||
if err2 != nil {
|
||||
return NewFileErrorFromName(err, realFilename)
|
||||
}
|
||||
defer f.Close()
|
||||
return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
|
||||
}
|
||||
|
||||
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
|
||||
realFilename := filename
|
||||
|
||||
// We want the most specific filename possible in the error message.
|
||||
fi, err2 := fs.Stat(filename)
|
||||
if err2 == nil {
|
||||
if s, ok := fi.(interface {
|
||||
Filename() string
|
||||
}); ok {
|
||||
realFilename = s.Filename()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
f, err2 := fs.Open(filename)
|
||||
if err2 != nil {
|
||||
return nil, realFilename, err2
|
||||
}
|
||||
|
||||
return f, realFilename, nil
|
||||
}
|
||||
|
||||
// Cause returns the underlying error or itself if it does not implement Unwrap.
|
||||
func Cause(err error) error {
|
||||
if u := errors.Unwrap(err); u != nil {
|
||||
return u
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func extractFileTypePos(err error) (string, text.Position) {
|
||||
err = Cause(err)
|
||||
|
||||
var fileType string
|
||||
|
||||
// LibSass, DartSass
|
||||
if pos := extractPosition(err); pos.LineNumber > 0 || pos.Offset > 0 {
|
||||
_, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
|
||||
return fileType, pos
|
||||
}
|
||||
|
||||
// Default to line 1 col 1 if we don't find any better.
|
||||
pos := text.Position{
|
||||
Offset: -1,
|
||||
LineNumber: 1,
|
||||
ColumnNumber: 1,
|
||||
}
|
||||
|
||||
// JSON errors.
|
||||
offset, typ := extractOffsetAndType(err)
|
||||
if fileType == "" {
|
||||
fileType = typ
|
||||
}
|
||||
|
||||
if offset >= 0 {
|
||||
pos.Offset = offset
|
||||
}
|
||||
|
||||
// The error type from the minifier contains line number and column number.
|
||||
if line, col := exctractLineNumberAndColumnNumber(err); line >= 0 {
|
||||
pos.LineNumber = line
|
||||
pos.ColumnNumber = col
|
||||
return fileType, pos
|
||||
}
|
||||
|
||||
// Look in the error message for the line number.
|
||||
for _, handle := range lineNumberExtractors {
|
||||
lno, col := handle(err)
|
||||
if lno > 0 {
|
||||
pos.ColumnNumber = col
|
||||
pos.LineNumber = lno
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if fileType == "" && pos.Filename != "" {
|
||||
_, fileType = paths.FileAndExtNoDelimiter(pos.Filename)
|
||||
}
|
||||
|
||||
return fileType, pos
|
||||
}
|
||||
|
||||
// UnwrapFileError tries to unwrap a FileError from err.
|
||||
// It returns nil if this is not possible.
|
||||
func UnwrapFileError(err error) FileError {
|
||||
for err != nil {
|
||||
switch v := err.(type) {
|
||||
case FileError:
|
||||
return v
|
||||
default:
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnwrapFileErrors tries to unwrap all FileError.
|
||||
func UnwrapFileErrors(err error) []FileError {
|
||||
var errs []FileError
|
||||
for err != nil {
|
||||
if v, ok := err.(FileError); ok {
|
||||
errs = append(errs, v)
|
||||
}
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
|
||||
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
|
||||
var errs []FileError
|
||||
for err != nil {
|
||||
if v, ok := err.(FileError); ok && v.ErrorContext() != nil {
|
||||
errs = append(errs, v)
|
||||
}
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func extractOffsetAndType(e error) (int, string) {
|
||||
switch v := e.(type) {
|
||||
case *json.UnmarshalTypeError:
|
||||
return int(v.Offset), "json"
|
||||
case *json.SyntaxError:
|
||||
return int(v.Offset), "json"
|
||||
default:
|
||||
return -1, ""
|
||||
}
|
||||
}
|
||||
|
||||
func exctractLineNumberAndColumnNumber(e error) (int, int) {
|
||||
switch v := e.(type) {
|
||||
case *parse.Error:
|
||||
return v.Line, v.Column
|
||||
case *toml.DecodeError:
|
||||
return v.Position()
|
||||
|
||||
}
|
||||
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func extractPosition(e error) (pos text.Position) {
|
||||
switch v := e.(type) {
|
||||
case godartsass.SassError:
|
||||
span := v.Span
|
||||
start := span.Start
|
||||
filename, _ := paths.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
|
||||
pos.ColumnNumber = v.Column
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,82 +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 (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestNewFileError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
fe := NewFileErrorFromName(errors.New("bar"), "foo.html")
|
||||
c.Assert(fe.Error(), qt.Equals, `"foo.html:1:1": bar`)
|
||||
|
||||
lines := ""
|
||||
for i := 1; i <= 100; i++ {
|
||||
lines += fmt.Sprintf("line %d\n", i)
|
||||
}
|
||||
|
||||
fe.UpdatePosition(text.Position{LineNumber: 32, ColumnNumber: 2})
|
||||
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:2": bar`)
|
||||
fe.UpdatePosition(text.Position{LineNumber: 0, ColumnNumber: 0, Offset: 212})
|
||||
fe.UpdateContent(strings.NewReader(lines), nil)
|
||||
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:0": bar`)
|
||||
errorContext := fe.ErrorContext()
|
||||
c.Assert(errorContext, qt.IsNotNil)
|
||||
c.Assert(errorContext.Lines, qt.DeepEquals, []string{"line 30", "line 31", "line 32", "line 33", "line 34"})
|
||||
c.Assert(errorContext.LinesPos, qt.Equals, 2)
|
||||
c.Assert(errorContext.ChromaLexer, qt.Equals, "go-html-template")
|
||||
|
||||
}
|
||||
|
||||
func TestNewFileErrorExtractFromMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
for i, test := range []struct {
|
||||
in error
|
||||
offset int
|
||||
lineNumber int
|
||||
columnNumber int
|
||||
}{
|
||||
{errors.New("no line number for you"), 0, 1, 1},
|
||||
{errors.New(`template: _default/single.html:4:15: executing "_default/single.html" at <.Titles>: can't evaluate field Titles in type *hugolib.PageOutput`), 0, 4, 15},
|
||||
{errors.New("parse failed: template: _default/bundle-resource-meta.html:11: unexpected in operand"), 0, 11, 1},
|
||||
{errors.New(`failed:: template: _default/bundle-resource-meta.html:2:7: executing "main" at <.Titles>`), 0, 2, 7},
|
||||
{errors.New(`failed to load translations: (6, 7): was expecting token =, but got "g" instead`), 0, 6, 7},
|
||||
{errors.New(`execute of template failed: template: index.html:2:5: executing "index.html" at <partial "foo.html" .>: error calling partial: "/layouts/partials/foo.html:3:6": execute of template failed: template: partials/foo.html:3:6: executing "partials/foo.html" at <.ThisDoesNotExist>: can't evaluate field ThisDoesNotExist in type *hugolib.pageStat`), 0, 2, 5},
|
||||
} {
|
||||
|
||||
got := NewFileErrorFromName(test.in, "test.txt")
|
||||
|
||||
errMsg := qt.Commentf("[%d][%T]", i, got)
|
||||
|
||||
pos := got.Position()
|
||||
c.Assert(pos.LineNumber, qt.Equals, test.lineNumber, errMsg)
|
||||
c.Assert(pos.ColumnNumber, qt.Equals, test.columnNumber, errMsg)
|
||||
c.Assert(errors.Unwrap(got), qt.Not(qt.IsNil))
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var lineNumberExtractors = []lineNumberExtractor{
|
||||
// Template/shortcode parse errors
|
||||
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
|
||||
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
|
||||
|
||||
// YAML parse errors
|
||||
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
|
||||
|
||||
// i18n bundle errors
|
||||
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
|
||||
}
|
||||
|
||||
type lineNumberExtractor func(e error) (int, int)
|
||||
|
||||
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
|
||||
re := regexp.MustCompile(expression)
|
||||
return extractLineNo(re)
|
||||
}
|
||||
|
||||
func extractLineNo(re *regexp.Regexp) lineNumberExtractor {
|
||||
return func(e error) (int, int) {
|
||||
if e == nil {
|
||||
panic("no error")
|
||||
}
|
||||
col := 1
|
||||
s := e.Error()
|
||||
m := re.FindStringSubmatch(s)
|
||||
if len(m) >= 2 {
|
||||
lno, _ := strconv.Atoi(m[1])
|
||||
if len(m) > 2 {
|
||||
col, _ = strconv.Atoi(m[2])
|
||||
}
|
||||
|
||||
if col <= 0 {
|
||||
col = 1
|
||||
}
|
||||
|
||||
return lno, col
|
||||
}
|
||||
|
||||
return 0, col
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hexec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/security"
|
||||
)
|
||||
|
||||
var WithDir = func(dir string) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.dir = dir
|
||||
}
|
||||
}
|
||||
|
||||
var WithContext = func(ctx context.Context) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
var WithStdout = func(w io.Writer) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.stdout = w
|
||||
}
|
||||
}
|
||||
|
||||
var WithStderr = func(w io.Writer) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.stderr = w
|
||||
}
|
||||
}
|
||||
|
||||
var WithStdin = func(r io.Reader) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
c.stdin = r
|
||||
}
|
||||
}
|
||||
|
||||
var WithEnviron = func(env []string) func(c *commandeer) {
|
||||
return func(c *commandeer) {
|
||||
setOrAppend := func(s string) {
|
||||
k1, _ := config.SplitEnvVar(s)
|
||||
var found bool
|
||||
for i, v := range c.env {
|
||||
k2, _ := config.SplitEnvVar(v)
|
||||
if k1 == k2 {
|
||||
found = true
|
||||
c.env[i] = s
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
c.env = append(c.env, s)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range env {
|
||||
setOrAppend(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Exec using the provided security config.
|
||||
func New(cfg security.Config) *Exec {
|
||||
var baseEnviron []string
|
||||
for _, v := range os.Environ() {
|
||||
k, _ := config.SplitEnvVar(v)
|
||||
if cfg.Exec.OsEnv.Accept(k) {
|
||||
baseEnviron = append(baseEnviron, v)
|
||||
}
|
||||
}
|
||||
|
||||
return &Exec{
|
||||
sc: cfg,
|
||||
baseEnviron: baseEnviron,
|
||||
}
|
||||
}
|
||||
|
||||
// IsNotFound reports whether this is an error about a binary not found.
|
||||
func IsNotFound(err error) bool {
|
||||
var notFoundErr *NotFoundError
|
||||
return errors.As(err, ¬FoundErr)
|
||||
}
|
||||
|
||||
// SafeCommand is a wrapper around os/exec Command which uses a LookPath
|
||||
// implementation that does not search in current directory before looking in PATH.
|
||||
// See https://github.com/cli/safeexec and the linked issues.
|
||||
func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
|
||||
bin, err := safeexec.LookPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exec.Command(bin, arg...), nil
|
||||
}
|
||||
|
||||
// Exec encorces a security policy for commands run via os/exec.
|
||||
type Exec struct {
|
||||
sc security.Config
|
||||
|
||||
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
|
||||
baseEnviron []string
|
||||
}
|
||||
|
||||
// New will fail if name is not allowed according to the configured security policy.
|
||||
// Else a configured Runner will be returned ready to be Run.
|
||||
func (e *Exec) New(name string, arg ...any) (Runner, error) {
|
||||
if err := e.sc.CheckAllowedExec(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
env := make([]string, len(e.baseEnviron))
|
||||
copy(env, e.baseEnviron)
|
||||
|
||||
cm := &commandeer{
|
||||
name: name,
|
||||
env: env,
|
||||
}
|
||||
|
||||
return cm.command(arg...)
|
||||
|
||||
}
|
||||
|
||||
// Npx is a convenience method to create a Runner running npx --no-install <name> <args.
|
||||
func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
|
||||
arg = append(arg[:0], append([]any{"--no-install", name}, arg[0:]...)...)
|
||||
return e.New("npx", arg...)
|
||||
}
|
||||
|
||||
// Sec returns the security policies this Exec is configured with.
|
||||
func (e *Exec) Sec() security.Config {
|
||||
return e.sc
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return fmt.Sprintf("binary with name %q not found", e.name)
|
||||
}
|
||||
|
||||
// Runner wraps a *os.Cmd.
|
||||
type Runner interface {
|
||||
Run() error
|
||||
StdinPipe() (io.WriteCloser, error)
|
||||
}
|
||||
|
||||
type cmdWrapper struct {
|
||||
name string
|
||||
c *exec.Cmd
|
||||
|
||||
outerr *bytes.Buffer
|
||||
}
|
||||
|
||||
var notFoundRe = regexp.MustCompile(`(?s)not found:|could not determine executable`)
|
||||
|
||||
func (c *cmdWrapper) Run() error {
|
||||
err := c.c.Run()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if notFoundRe.MatchString(c.outerr.String()) {
|
||||
return &NotFoundError{name: c.name}
|
||||
}
|
||||
return fmt.Errorf("failed to execute binary %q with args %v: %s", c.name, c.c.Args[1:], c.outerr.String())
|
||||
}
|
||||
|
||||
func (c *cmdWrapper) StdinPipe() (io.WriteCloser, error) {
|
||||
return c.c.StdinPipe()
|
||||
}
|
||||
|
||||
type commandeer struct {
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
stdin io.Reader
|
||||
dir string
|
||||
ctx context.Context
|
||||
|
||||
name string
|
||||
env []string
|
||||
}
|
||||
|
||||
func (c *commandeer) command(arg ...any) (*cmdWrapper, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var args []string
|
||||
for _, a := range arg {
|
||||
switch v := a.(type) {
|
||||
case string:
|
||||
args = append(args, v)
|
||||
case func(*commandeer):
|
||||
v(c)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid argument to command: %T", a)
|
||||
}
|
||||
}
|
||||
|
||||
bin, err := safeexec.LookPath(c.name)
|
||||
if err != nil {
|
||||
return nil, &NotFoundError{
|
||||
name: c.name,
|
||||
}
|
||||
}
|
||||
|
||||
outerr := &bytes.Buffer{}
|
||||
if c.stderr == nil {
|
||||
c.stderr = outerr
|
||||
} else {
|
||||
c.stderr = io.MultiWriter(c.stderr, outerr)
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if c.ctx != nil {
|
||||
cmd = exec.CommandContext(c.ctx, bin, args...)
|
||||
} else {
|
||||
cmd = exec.Command(bin, args...)
|
||||
}
|
||||
|
||||
cmd.Stdin = c.stdin
|
||||
cmd.Stderr = c.stderr
|
||||
cmd.Stdout = c.stdout
|
||||
cmd.Env = c.env
|
||||
cmd.Dir = c.dir
|
||||
|
||||
return &cmdWrapper{outerr: outerr, c: cmd, name: c.name}, nil
|
||||
}
|
||||
|
||||
// InPath reports whether binaryName is in $PATH.
|
||||
func InPath(binaryName string) bool {
|
||||
if strings.Contains(binaryName, "/") {
|
||||
panic("binary name should not contain any slash")
|
||||
}
|
||||
_, err := safeexec.LookPath(binaryName)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// LookPath finds the path to binaryName in $PATH.
|
||||
// Returns "" if not found.
|
||||
func LookPath(binaryName string) string {
|
||||
if strings.Contains(binaryName, "/") {
|
||||
panic("binary name should not contain any slash")
|
||||
}
|
||||
s, err := safeexec.LookPath(binaryName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package hreflect contains reflect helpers.
|
||||
package hreflect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
// TODO(bep) replace the private versions in /tpl with these.
|
||||
// IsNumber returns whether the given kind is a number.
|
||||
func IsNumber(kind reflect.Kind) bool {
|
||||
return IsInt(kind) || IsUint(kind) || IsFloat(kind)
|
||||
}
|
||||
|
||||
// IsInt returns whether the given kind is an int.
|
||||
func IsInt(kind reflect.Kind) bool {
|
||||
switch kind {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsUint returns whether the given kind is an uint.
|
||||
func IsUint(kind reflect.Kind) bool {
|
||||
switch kind {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsFloat returns whether the given kind is a float.
|
||||
func IsFloat(kind reflect.Kind) bool {
|
||||
switch kind {
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsTruthful returns whether in represents a truthful value.
|
||||
// See IsTruthfulValue
|
||||
func IsTruthful(in any) bool {
|
||||
switch v := in.(type) {
|
||||
case reflect.Value:
|
||||
return IsTruthfulValue(v)
|
||||
default:
|
||||
return IsTruthfulValue(reflect.ValueOf(in))
|
||||
}
|
||||
}
|
||||
|
||||
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
|
||||
|
||||
// IsTruthfulValue returns whether the given value has a meaningful truth value.
|
||||
// This is based on template.IsTrue in Go's stdlib, but also considers
|
||||
// IsZero and any interface value will be unwrapped before it's considered
|
||||
// for truthfulness.
|
||||
//
|
||||
// Based on:
|
||||
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
|
||||
func IsTruthfulValue(val reflect.Value) (truth bool) {
|
||||
val = indirectInterface(val)
|
||||
|
||||
if !val.IsValid() {
|
||||
// Something like var x interface{}, never set. It's a form of nil.
|
||||
return
|
||||
}
|
||||
|
||||
if val.Type().Implements(zeroType) {
|
||||
return !val.Interface().(types.Zeroer).IsZero()
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
truth = val.Len() > 0
|
||||
case reflect.Bool:
|
||||
truth = val.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
truth = val.Complex() != 0
|
||||
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
|
||||
truth = !val.IsNil()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
truth = val.Int() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
truth = val.Float() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
truth = val.Uint() != 0
|
||||
case reflect.Struct:
|
||||
truth = true // Struct values are always true.
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type methodKey struct {
|
||||
typ reflect.Type
|
||||
name string
|
||||
}
|
||||
|
||||
type methods struct {
|
||||
sync.RWMutex
|
||||
cache map[methodKey]int
|
||||
}
|
||||
|
||||
var methodCache = &methods{cache: make(map[methodKey]int)}
|
||||
|
||||
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
|
||||
// type lookup.
|
||||
func GetMethodByName(v reflect.Value, name string) reflect.Value {
|
||||
index := GetMethodIndexByName(v.Type(), name)
|
||||
|
||||
if index == -1 {
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
return v.Method(index)
|
||||
}
|
||||
|
||||
// GetMethodIndexByName returns the index of the method with the given name, or
|
||||
// -1 if no such method exists.
|
||||
func GetMethodIndexByName(tp reflect.Type, name string) int {
|
||||
k := methodKey{tp, name}
|
||||
methodCache.RLock()
|
||||
index, found := methodCache.cache[k]
|
||||
methodCache.RUnlock()
|
||||
if found {
|
||||
return index
|
||||
}
|
||||
|
||||
methodCache.Lock()
|
||||
defer methodCache.Unlock()
|
||||
|
||||
m, ok := tp.MethodByName(name)
|
||||
index = m.Index
|
||||
if !ok {
|
||||
index = -1
|
||||
}
|
||||
methodCache.cache[k] = index
|
||||
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
|
||||
return m.Index
|
||||
}
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||
asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem()
|
||||
)
|
||||
|
||||
// IsTime returns whether tp is a time.Time type or if it can be converted into one
|
||||
// in ToTime.
|
||||
func IsTime(tp reflect.Type) bool {
|
||||
if tp == timeType {
|
||||
return true
|
||||
}
|
||||
|
||||
if tp.Implements(asTimeProviderType) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AsTime returns v as a time.Time if possible.
|
||||
// The given location is only used if the value implements AsTimeProvider (e.g. go-toml local).
|
||||
// A zero Time and false is returned if this isn't possible.
|
||||
// Note that this function does not accept string dates.
|
||||
func AsTime(v reflect.Value, loc *time.Location) (time.Time, bool) {
|
||||
if v.Kind() == reflect.Interface {
|
||||
return AsTime(v.Elem(), loc)
|
||||
}
|
||||
|
||||
if v.Type() == timeType {
|
||||
return v.Interface().(time.Time), true
|
||||
}
|
||||
|
||||
if v.Type().Implements(asTimeProviderType) {
|
||||
return v.Interface().(htime.AsTimeProvider).AsTime(loc), true
|
||||
}
|
||||
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
|
||||
func indirectInterface(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Interface {
|
||||
return v
|
||||
}
|
||||
if v.IsNil() {
|
||||
return reflect.Value{}
|
||||
}
|
||||
return v.Elem()
|
||||
}
|
||||
|
||||
var ContextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hreflect
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestIsTruthful(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(IsTruthful(true), qt.Equals, true)
|
||||
c.Assert(IsTruthful(false), qt.Equals, false)
|
||||
c.Assert(IsTruthful(time.Now()), qt.Equals, true)
|
||||
c.Assert(IsTruthful(time.Time{}), qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestGetMethodByName(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
v := reflect.ValueOf(&testStruct{})
|
||||
tp := v.Type()
|
||||
|
||||
c.Assert(GetMethodIndexByName(tp, "Method1"), qt.Equals, 0)
|
||||
c.Assert(GetMethodIndexByName(tp, "Method3"), qt.Equals, 2)
|
||||
c.Assert(GetMethodIndexByName(tp, "Foo"), qt.Equals, -1)
|
||||
}
|
||||
|
||||
func BenchmarkIsTruthFul(b *testing.B) {
|
||||
v := reflect.ValueOf("Hugo")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsTruthfulValue(v) {
|
||||
b.Fatal("not truthful")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testStruct struct{}
|
||||
|
||||
func (t *testStruct) Method1() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func (t *testStruct) Method2() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func (t *testStruct) Method3() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func (t *testStruct) Method4() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func (t *testStruct) Method5() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func BenchmarkGetMethodByName(b *testing.B) {
|
||||
v := reflect.ValueOf(&testStruct{})
|
||||
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, method := range methods {
|
||||
_ = GetMethodByName(v, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// 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
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/clock"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/locales"
|
||||
)
|
||||
|
||||
var (
|
||||
longDayNames = []string{
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
}
|
||||
|
||||
shortDayNames = []string{
|
||||
"Sun",
|
||||
"Mon",
|
||||
"Tue",
|
||||
"Wed",
|
||||
"Thu",
|
||||
"Fri",
|
||||
"Sat",
|
||||
}
|
||||
|
||||
shortMonthNames = []string{
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
}
|
||||
|
||||
longMonthNames = []string{
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
}
|
||||
|
||||
Clock = clock.System()
|
||||
)
|
||||
|
||||
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
|
||||
if ltr == nil {
|
||||
panic("must provide a locales.Translator")
|
||||
}
|
||||
return TimeFormatter{
|
||||
ltr: ltr,
|
||||
}
|
||||
}
|
||||
|
||||
// TimeFormatter is locale aware.
|
||||
type TimeFormatter struct {
|
||||
ltr locales.Translator
|
||||
}
|
||||
|
||||
func (f TimeFormatter) Format(t time.Time, layout string) string {
|
||||
if layout == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if layout[0] == ':' {
|
||||
// It may be one of Hugo's custom layouts.
|
||||
switch strings.ToLower(layout[1:]) {
|
||||
case "date_full":
|
||||
return f.ltr.FmtDateFull(t)
|
||||
case "date_long":
|
||||
return f.ltr.FmtDateLong(t)
|
||||
case "date_medium":
|
||||
return f.ltr.FmtDateMedium(t)
|
||||
case "date_short":
|
||||
return f.ltr.FmtDateShort(t)
|
||||
case "time_full":
|
||||
return f.ltr.FmtTimeFull(t)
|
||||
case "time_long":
|
||||
return f.ltr.FmtTimeLong(t)
|
||||
case "time_medium":
|
||||
return f.ltr.FmtTimeMedium(t)
|
||||
case "time_short":
|
||||
return f.ltr.FmtTimeShort(t)
|
||||
}
|
||||
}
|
||||
|
||||
s := t.Format(layout)
|
||||
|
||||
monthIdx := t.Month() - 1 // Month() starts at 1.
|
||||
dayIdx := t.Weekday()
|
||||
|
||||
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()))
|
||||
}
|
||||
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()))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func ToTimeInDefaultLocationE(i any, location *time.Location) (tim time.Time, err error) {
|
||||
switch vv := i.(type) {
|
||||
case AsTimeProvider:
|
||||
return vv.AsTime(location), nil
|
||||
// issue #8895
|
||||
// datetimes parsed by `go-toml` have empty zone name
|
||||
// convert back them into string and use `cast`
|
||||
// TODO(bep) add tests, make sure we really need this.
|
||||
case time.Time:
|
||||
i = vv.Format(time.RFC3339)
|
||||
}
|
||||
return cast.ToTimeInDefaultLocationE(i, location)
|
||||
}
|
||||
|
||||
// Now returns time.Now() or time value based on the `clock` flag.
|
||||
// Use this function to fake time inside hugo.
|
||||
func Now() time.Time {
|
||||
return Clock.Now()
|
||||
}
|
||||
|
||||
func Since(t time.Time) time.Duration {
|
||||
return Clock.Since(t)
|
||||
}
|
||||
|
||||
// AsTimeProvider is implemented by go-toml's LocalDate and LocalDateTime.
|
||||
type AsTimeProvider interface {
|
||||
AsTime(zone *time.Location) time.Time
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
translators "github.com/gohugoio/localescompressed"
|
||||
)
|
||||
|
||||
func TestTimeFormatter(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
|
||||
june06 = june06.Add(7777 * time.Second)
|
||||
|
||||
jan06, _ := time.Parse("2006-Jan-02", "2018-Jan-06")
|
||||
jan06 = jan06.Add(32 * time.Second)
|
||||
|
||||
mondayNovemberFirst, _ := time.Parse("2006-Jan-02", "2021-11-01")
|
||||
mondayNovemberFirst = mondayNovemberFirst.Add(33 * time.Second)
|
||||
|
||||
c.Run("Norsk nynorsk", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
|
||||
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "onsdag juni 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "on. juni 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon Mon"), qt.Equals, "on. on.")
|
||||
})
|
||||
|
||||
c.Run("Custom layouts Norsk nynorsk", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
|
||||
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "onsdag 6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.2018")
|
||||
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "kl. 02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "02:09:37")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "02:09")
|
||||
|
||||
})
|
||||
|
||||
c.Run("Custom layouts English", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("en"))
|
||||
|
||||
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "Wednesday, June 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "June 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "Jun 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "6/6/18")
|
||||
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37 am UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37 am UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37 am")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09 am")
|
||||
|
||||
})
|
||||
|
||||
c.Run("English", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("en"))
|
||||
|
||||
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "Wednesday Jun 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "Wed June 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon Mon"), qt.Equals, "Wed Wed")
|
||||
})
|
||||
|
||||
c.Run("Weekdays German", func(c *qt.C) {
|
||||
tr := translators.GetTranslator("de")
|
||||
f := NewTimeFormatter(tr)
|
||||
|
||||
// Issue #9107
|
||||
for i, weekDayWideGerman := range []string{"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"} {
|
||||
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
|
||||
c.Assert(tr.WeekdayWide(date.Weekday()), qt.Equals, weekDayWideGerman)
|
||||
c.Assert(f.Format(date, "Monday"), qt.Equals, weekDayWideGerman)
|
||||
}
|
||||
|
||||
for i, weekDayAbbreviatedGerman := range []string{"Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa.", "So."} {
|
||||
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
|
||||
c.Assert(tr.WeekdayAbbreviated(date.Weekday()), qt.Equals, weekDayAbbreviatedGerman)
|
||||
c.Assert(f.Format(date, "Mon"), qt.Equals, weekDayAbbreviatedGerman)
|
||||
}
|
||||
})
|
||||
|
||||
c.Run("Months German", func(c *qt.C) {
|
||||
tr := translators.GetTranslator("de")
|
||||
f := NewTimeFormatter(tr)
|
||||
|
||||
// Issue #9107
|
||||
for i, monthWideNorway := range []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli"} {
|
||||
date := jan06.Add(time.Duration(i*24*31) * time.Hour)
|
||||
c.Assert(tr.MonthWide(date.Month()), qt.Equals, monthWideNorway)
|
||||
c.Assert(f.Format(date, "January"), qt.Equals, monthWideNorway)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkTimeFormatter(b *testing.B) {
|
||||
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
|
||||
|
||||
b.Run("Native", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := june06.Format("Monday Jan 2 2006")
|
||||
if got != "Wednesday Jun 6 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Localized", func(b *testing.B) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := f.Format(june06, "Monday Jan 2 2006")
|
||||
if got != "onsdag juni 6 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Localized Custom", func(b *testing.B) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := f.Format(june06, ":date_medium")
|
||||
if got != "6. juni 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// CopyFile copies a file.
|
||||
func CopyFile(fs afero.Fs, from, to string) error {
|
||||
sf, err := fs.Open(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sf.Close()
|
||||
df, err := fs.Create(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer df.Close()
|
||||
_, err = io.Copy(df, sf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
si, err := fs.Stat(from)
|
||||
if err != nil {
|
||||
err = fs.Chmod(to, si.Mode())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyDir copies a directory.
|
||||
func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool) error {
|
||||
fi, err := os.Stat(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
return fmt.Errorf("%q is not a directory", from)
|
||||
}
|
||||
|
||||
err = fs.MkdirAll(to, 0777) // before umask
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, _ := ioutil.ReadDir(from)
|
||||
for _, entry := range entries {
|
||||
fromFilename := filepath.Join(from, entry.Name())
|
||||
toFilename := filepath.Join(to, entry.Name())
|
||||
if entry.IsDir() {
|
||||
if shouldCopy != nil && !shouldCopy(fromFilename) {
|
||||
continue
|
||||
}
|
||||
if err := CopyDir(fs, fromFilename, toFilename, shouldCopy); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := CopyFile(fs, fromFilename, toFilename); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user