diff --git a/.gitignore b/.gitignore
index de359fc21..47721b7cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@ vendor/*/
*.bench
coverage*.out
-GoBuilds
\ No newline at end of file
+GoBuilds
+dist
diff --git a/commands/genman.go b/commands/genman.go
index 7b26afeed..cd5c8c84e 100644
--- a/commands/genman.go
+++ b/commands/genman.go
@@ -36,7 +36,7 @@ in the "man" directory under the current directory.`,
header := &doc.GenManHeader{
Section: "1",
Manual: "Hugo Manual",
- Source: fmt.Sprintf("Hugo %s", helpers.HugoVersion()),
+ Source: fmt.Sprintf("Hugo %s", helpers.CurrentHugoVersion),
}
if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) {
genmandir += helpers.FilePathSeparator
diff --git a/commands/hugo.go b/commands/hugo.go
index 73dde5d2d..4260f6e56 100644
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -399,7 +399,7 @@ func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) {
if themeVersionMismatch {
cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
- helpers.HugoReleaseVersion(), minVersion)
+ helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
}
return cfg, nil
diff --git a/commands/release.go b/commands/release.go
new file mode 100644
index 000000000..f6d98e29f
--- /dev/null
+++ b/commands/release.go
@@ -0,0 +1,62 @@
+// +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 (
+ "github.com/spf13/cobra"
+ "github.com/spf13/hugo/releaser"
+)
+
+func init() {
+ HugoCmd.AddCommand(createReleaser().cmd)
+}
+
+type releaseCommandeer struct {
+ cmd *cobra.Command
+
+ // Will be zero for main releases.
+ patchLevel int
+
+ skipPublish bool
+
+ step int
+}
+
+func createReleaser() *releaseCommandeer {
+ // Note: This is a command only meant for internal use and must be run
+ // via "go run -tags release main.go release" on the actual code base that is in the release.
+ r := &releaseCommandeer{
+ cmd: &cobra.Command{
+ Use: "release",
+ Short: "Release a new version of Hugo.",
+ Hidden: true,
+ },
+ }
+
+ r.cmd.RunE = func(cmd *cobra.Command, args []string) error {
+ return r.release()
+ }
+
+ r.cmd.PersistentFlags().IntVarP(&r.patchLevel, "patch", "p", 0, "Patch level, defaults to 0 for main releases")
+ r.cmd.PersistentFlags().IntVarP(&r.step, "step", "s", -1, "Release step, defaults to -1 for all steps.")
+ r.cmd.PersistentFlags().BoolVarP(&r.skipPublish, "skip-publish", "", false, "Skip all publishing pipes of the release")
+
+ return r
+}
+
+func (r *releaseCommandeer) release() error {
+ return releaser.New(r.patchLevel, r.step, r.skipPublish).Run()
+}
diff --git a/commands/version.go b/commands/version.go
index f026c13a5..9e673b817 100644
--- a/commands/version.go
+++ b/commands/version.go
@@ -44,9 +44,9 @@ func printHugoVersion() {
formatBuildDate() // format the compile time
}
if hugolib.CommitHash == "" {
- jww.FEEDBACK.Printf("Hugo Static Site Generator v%s %s/%s BuildDate: %s\n", helpers.HugoVersion(), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
+ jww.FEEDBACK.Printf("Hugo Static Site Generator v%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
} else {
- jww.FEEDBACK.Printf("Hugo Static Site Generator v%s-%s %s/%s BuildDate: %s\n", helpers.HugoVersion(), strings.ToUpper(hugolib.CommitHash), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
+ jww.FEEDBACK.Printf("Hugo Static Site Generator v%s-%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, strings.ToUpper(hugolib.CommitHash), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
}
}
diff --git a/docs/config.toml b/docs/config.toml
index 25288342b..8d91a66bd 100644
--- a/docs/config.toml
+++ b/docs/config.toml
@@ -45,6 +45,11 @@ pluralizeListTitles = false
identifier = "about"
pre = ""
weight = -110
+[[menu.main]]
+ name = "Release Notes"
+ url = "/release-notes/"
+ pre = ""
+ weight = -111
[[menu.main]]
name = "Getting Started"
identifier = "getting started"
diff --git a/docs/content/release-notes/_index.md b/docs/content/release-notes/_index.md
new file mode 100644
index 000000000..3b934c69d
--- /dev/null
+++ b/docs/content/release-notes/_index.md
@@ -0,0 +1,8 @@
+---
+date: 2017-04-17
+aliases:
+- /doc/release-notes/
+- /meta/release-notes/
+title: Release Notes
+weight: 10
+---
diff --git a/docs/content/meta/release-notes.md b/docs/content/release-notes/release-notes.md
similarity index 99%
rename from docs/content/meta/release-notes.md
rename to docs/content/release-notes/release-notes.md
index af82d908a..7f962e318 100644
--- a/docs/content/meta/release-notes.md
+++ b/docs/content/release-notes/release-notes.md
@@ -2,12 +2,8 @@
aliases:
- /doc/release-notes/
- /meta/release-notes/
-date: 2013-07-01
-menu:
- main:
- parent: about
-title: Release Notes
-weight: 10
+date: 2017-04-16
+title: Older Release Notes
---
# **0.20.2** April 16th 2017
diff --git a/docs/layouts/section/release-notes.html b/docs/layouts/section/release-notes.html
new file mode 100644
index 000000000..6af512603
--- /dev/null
+++ b/docs/layouts/section/release-notes.html
@@ -0,0 +1,6 @@
+{{ define "main" }}
+{{ range .Pages }}
+
{{ .Title }} {{ .Date.Format "Jan 2, 2006" }}
+{{ .Content }}
+{{ end }}
+{{ end }}
\ No newline at end of file
diff --git a/goreleaser.yml b/goreleaser.yml
new file mode 100644
index 000000000..4b7fef63a
--- /dev/null
+++ b/goreleaser.yml
@@ -0,0 +1,48 @@
+build:
+ main: main.go
+ binary: hugo
+ ldflags_template: -s -w -X hugolib.BuildDate={{.Date}}
+ goos:
+ - darwin
+ - linux
+ - windows
+ - freebsd
+ - netbsd
+ - openbsd
+ - dragonfly
+ goarch:
+ - amd64
+ - 386
+ - arm
+ - arm64
+fpm:
+ formats:
+ - deb
+ vendor: "gohugo.io"
+ url: "https://gohugo.io/"
+ maintainer: ""
+ description: "A Fast and Flexible Static Site Generator built with love in GoLang."
+ license: "Apache 2.0"
+archive:
+ format: tar.gz
+ format_overrides:
+ - goos: windows
+ format: zip
+ name_template: "{{.Binary}}_{{.Version}}_{{.Os}}-{{.Arch}}"
+ replacements:
+ amd64: 64bit
+ 386: 32bit
+ arm: ARM
+ arm64: ARM64
+ darwin: macOS
+ linux: Linux
+ windows: Windows
+ openbsd: OpenBSD
+ netbsd: NetBSD
+ freebsd: FreeBSD
+ dragonfly: DragonFlyBSD
+ files:
+ - README.md
+ - LICENSE.md
+release:
+ draft: true
diff --git a/helpers/general.go b/helpers/general.go
index ac2af4936..ea3620119 100644
--- a/helpers/general.go
+++ b/helpers/general.go
@@ -286,7 +286,7 @@ func InitLoggers() {
// plenty of time to fix their templates.
func Deprecated(object, item, alternative string, err bool) {
if err {
- DistinctErrorLog.Printf("%s's %s is deprecated and will be removed in Hugo %s. %s.", object, item, NextHugoReleaseVersion(), alternative)
+ DistinctErrorLog.Printf("%s's %s is deprecated and will be removed in Hugo %s. %s.", object, item, CurrentHugoVersion.Next().ReleaseVersion(), alternative)
} else {
// Make sure the users see this while avoiding build breakage. This will not lead to an os.Exit(-1)
diff --git a/helpers/hugo.go b/helpers/hugo.go
index 2fda7cdc4..05e47306c 100644
--- a/helpers/hugo.go
+++ b/helpers/hugo.go
@@ -20,35 +20,51 @@ import (
"github.com/spf13/cast"
)
-// HugoVersionNumber represents the current build version.
-// This should be the only one
-const (
+// HugoVersion represents the Hugo build version.
+type HugoVersion struct {
// Major and minor version.
- HugoVersionNumber = 0.21
+ Number float32
// Increment this for bug releases
- HugoPatchVersion = 0
-)
+ PatchLevel int
-// HugoVersionSuffix is the suffix used in the Hugo version string.
-// It will be blank for release versions.
-const HugoVersionSuffix = "-DEV" // use this when not doing a release
-//const HugoVersionSuffix = "" // use this line when doing a release
-
-// HugoVersion returns the current Hugo version. It will include
-// a suffix, typically '-DEV', if it's development version.
-func HugoVersion() string {
- return hugoVersion(HugoVersionNumber, HugoPatchVersion, HugoVersionSuffix)
+ // HugoVersionSuffix is the suffix used in the Hugo version string.
+ // It will be blank for release versions.
+ Suffix string
}
-// HugoReleaseVersion is same as HugoVersion, but no suffix.
-func HugoReleaseVersion() string {
- return hugoVersionNoSuffix(HugoVersionNumber, HugoPatchVersion)
+func (v HugoVersion) String() string {
+ return hugoVersion(v.Number, v.PatchLevel, v.Suffix)
}
-// NextHugoReleaseVersion returns the next Hugo release version.
-func NextHugoReleaseVersion() string {
- return hugoVersionNoSuffix(HugoVersionNumber+0.01, 0)
+// ReleaseVersion represents the release version.
+func (v HugoVersion) ReleaseVersion() HugoVersion {
+ v.Suffix = ""
+ return v
+}
+
+// Next returns the next Hugo release version.
+func (v HugoVersion) Next() HugoVersion {
+ return HugoVersion{Number: v.Number + 0.01}
+}
+
+// Pre returns the previous Hugo release version.
+func (v HugoVersion) Prev() HugoVersion {
+ return HugoVersion{Number: v.Number - 0.01}
+}
+
+// NextPatchLevel returns the next patch/bugfix Hugo version.
+// This will be a patch increment on the previous Hugo version.
+func (v HugoVersion) NextPatchLevel(level int) HugoVersion {
+ return HugoVersion{Number: v.Number - 0.01, PatchLevel: level}
+}
+
+// CurrentHugoVersion represents the current build version.
+// This should be the only one.
+var CurrentHugoVersion = HugoVersion{
+ Number: 0.21,
+ PatchLevel: 0,
+ Suffix: "-DEV",
}
func hugoVersion(version float32, patchVersion int, suffix string) string {
@@ -58,19 +74,12 @@ func hugoVersion(version float32, patchVersion int, suffix string) string {
return fmt.Sprintf("%.2f%s", version, suffix)
}
-func hugoVersionNoSuffix(version float32, patchVersion int) string {
- if patchVersion > 0 {
- return fmt.Sprintf("%.2f.%d", version, patchVersion)
- }
- return fmt.Sprintf("%.2f", version)
-}
-
// CompareVersion compares the given version string or number against the
// running Hugo version.
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
// the running version.
func CompareVersion(version interface{}) int {
- return compareVersions(HugoVersionNumber, HugoPatchVersion, version)
+ return compareVersions(CurrentHugoVersion.Number, CurrentHugoVersion.PatchLevel, version)
}
func compareVersions(inVersion float32, inPatchVersion int, in interface{}) int {
diff --git a/helpers/hugo_test.go b/helpers/hugo_test.go
index b71517f71..c96a1351b 100644
--- a/helpers/hugo_test.go
+++ b/helpers/hugo_test.go
@@ -22,10 +22,14 @@ import (
func TestHugoVersion(t *testing.T) {
assert.Equal(t, "0.15-DEV", hugoVersion(0.15, 0, "-DEV"))
- assert.Equal(t, "0.17", hugoVersionNoSuffix(0.16+0.01, 0))
- assert.Equal(t, "0.20", hugoVersionNoSuffix(0.20, 0))
assert.Equal(t, "0.15.2-DEV", hugoVersion(0.15, 2, "-DEV"))
- assert.Equal(t, "0.17.3", hugoVersionNoSuffix(0.16+0.01, 3))
+
+ v := HugoVersion{Number: 0.21, PatchLevel: 0, Suffix: "-DEV"}
+
+ require.Equal(t, v.ReleaseVersion().String(), "0.21")
+ require.Equal(t, "0.21-DEV", v.String())
+ require.Equal(t, "0.22", v.Next().String())
+ require.Equal(t, "0.20.3", v.NextPatchLevel(3).String())
}
func TestCompareVersions(t *testing.T) {
diff --git a/hugolib/hugo_info.go b/hugolib/hugo_info.go
index 77bf54802..86842f38b 100644
--- a/hugolib/hugo_info.go
+++ b/hugolib/hugo_info.go
@@ -41,9 +41,9 @@ type HugoInfo struct {
func init() {
hugoInfo = &HugoInfo{
- Version: helpers.HugoVersion(),
+ Version: helpers.CurrentHugoVersion.String(),
CommitHash: CommitHash,
BuildDate: BuildDate,
- Generator: template.HTML(fmt.Sprintf(``, helpers.HugoVersion())),
+ Generator: template.HTML(fmt.Sprintf(``, helpers.CurrentHugoVersion.String())),
}
}
diff --git a/releaser/git.go b/releaser/git.go
new file mode 100644
index 000000000..d8b5bef31
--- /dev/null
+++ b/releaser/git.go
@@ -0,0 +1,265 @@
+// 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 releaser
+
+import (
+ "fmt"
+ "os/exec"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+var issueRe = regexp.MustCompile(`(?i)[Updates?|Closes?|Fix.*|See] #(\d+)`)
+
+const (
+ templateChanges = "templateChanges"
+ coreChanges = "coreChanges"
+ outChanges = "outChanges"
+ docsChanges = "docsChanges"
+ otherChanges = "otherChanges"
+)
+
+type changeLog struct {
+ Version string
+ Enhancements map[string]gitInfos
+ Fixes map[string]gitInfos
+ All gitInfos
+
+ // Overall stats
+ Repo *gitHubRepo
+ ContributorCount int
+ ThemeCount int
+}
+
+func newChangeLog(infos gitInfos) changeLog {
+ return changeLog{
+ Enhancements: make(map[string]gitInfos),
+ Fixes: make(map[string]gitInfos),
+ All: infos,
+ }
+}
+
+func (l changeLog) addGitInfo(isFix bool, info gitInfo, category string) {
+ var (
+ infos gitInfos
+ found bool
+ segment map[string]gitInfos
+ )
+
+ if isFix {
+ segment = l.Fixes
+ } else {
+ segment = l.Enhancements
+ }
+
+ infos, found = segment[category]
+ if !found {
+ infos = gitInfos{}
+ }
+
+ infos = append(infos, info)
+ segment[category] = infos
+}
+
+func gitInfosToChangeLog(infos gitInfos) changeLog {
+ log := newChangeLog(infos)
+ for _, info := range infos {
+ los := strings.ToLower(info.Subject)
+ isFix := strings.Contains(los, "fix")
+ var category = otherChanges
+
+ // TODO(bep) improve
+ if regexp.MustCompile("(?i)tpl:|tplimpl:|layout").MatchString(los) {
+ category = templateChanges
+ } else if regexp.MustCompile("(?i)docs?:|documentation:").MatchString(los) {
+ category = docsChanges
+ } else if regexp.MustCompile("(?i)hugolib:").MatchString(los) {
+ category = coreChanges
+ } else if regexp.MustCompile("(?i)out(put)?:|media:|Output|Media").MatchString(los) {
+ category = outChanges
+ }
+
+ // Trim package prefix.
+ colonIdx := strings.Index(info.Subject, ":")
+ if colonIdx != -1 && colonIdx < (len(info.Subject)/2) {
+ info.Subject = info.Subject[colonIdx+1:]
+ }
+
+ info.Subject = strings.TrimSpace(info.Subject)
+
+ log.addGitInfo(isFix, info, category)
+ }
+
+ return log
+}
+
+type gitInfo struct {
+ Hash string
+ Author string
+ Subject string
+ Body string
+
+ GitHubCommit *gitHubCommit
+}
+
+func (g gitInfo) Issues() []int {
+ return extractIssues(g.Body)
+}
+
+func (g gitInfo) AuthorID() string {
+ if g.GitHubCommit != nil {
+ return g.GitHubCommit.Author.Login
+ }
+ return g.Author
+}
+
+func extractIssues(body string) []int {
+ var i []int
+ m := issueRe.FindAllStringSubmatch(body, -1)
+ for _, mm := range m {
+ issueID, err := strconv.Atoi(mm[1])
+ if err != nil {
+ continue
+ }
+ i = append(i, issueID)
+ }
+ return i
+}
+
+type gitInfos []gitInfo
+
+func git(args ...string) (string, error) {
+ cmd := exec.Command("git", args...)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("git failed: %q: %q", err, out)
+ }
+ return string(out), nil
+}
+
+func getGitInfos(remote bool) (gitInfos, error) {
+ return getGitInfosBefore("HEAD", remote)
+}
+
+type countribCount struct {
+ Author string
+ GitHubAuthor gitHubAuthor
+ Count int
+}
+
+func (c countribCount) AuthorLink() string {
+ if c.GitHubAuthor.HtmlURL != "" {
+ return fmt.Sprintf("[@%s](%s)", c.GitHubAuthor.Login, c.GitHubAuthor.HtmlURL)
+ }
+
+ if !strings.Contains(c.Author, "@") {
+ return c.Author
+ }
+
+ return c.Author[:strings.Index(c.Author, "@")]
+
+}
+
+type contribCounts []countribCount
+
+func (c contribCounts) Less(i, j int) bool { return c[i].Count > c[j].Count }
+func (c contribCounts) Len() int { return len(c) }
+func (c contribCounts) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
+
+func (g gitInfos) ContribCountPerAuthor() contribCounts {
+ var c contribCounts
+
+ counters := make(map[string]countribCount)
+
+ for _, gi := range g {
+ authorID := gi.AuthorID()
+ if count, ok := counters[authorID]; ok {
+ count.Count = count.Count + 1
+ counters[authorID] = count
+ } else {
+ var ghA gitHubAuthor
+ if gi.GitHubCommit != nil {
+ ghA = gi.GitHubCommit.Author
+ }
+ authorCount := countribCount{Count: 1, Author: gi.Author, GitHubAuthor: ghA}
+ counters[authorID] = authorCount
+ }
+ }
+
+ for _, v := range counters {
+ c = append(c, v)
+ }
+
+ sort.Sort(c)
+ return c
+}
+
+func getGitInfosBefore(ref string, remote bool) (gitInfos, error) {
+
+ var g gitInfos
+
+ log, err := gitLogBefore(ref)
+ if err != nil {
+ return g, err
+ }
+
+ log = strings.Trim(log, "\n\x1e'")
+ entries := strings.Split(log, "\x1e")
+
+ for _, entry := range entries {
+ items := strings.Split(entry, "\x1f")
+ gi := gitInfo{
+ Hash: items[0],
+ Author: items[1],
+ Subject: items[2],
+ Body: items[3],
+ }
+ if remote {
+ gc, err := fetchCommit(gi.Hash)
+ if err == nil {
+ gi.GitHubCommit = &gc
+ }
+ }
+ g = append(g, gi)
+ }
+
+ return g, nil
+}
+
+// Ignore autogenerated commits etc. in change log. This is a regexp.
+const ignoredCommits = "release:|vendor:|snapcraft:"
+
+func gitLogBefore(ref string) (string, error) {
+ prevTag, err := gitShort("describe", "--tags", "--abbrev=0", "--always", ref+"^")
+ if err != nil {
+ return "", err
+ }
+ log, err := git("log", "-E", fmt.Sprintf("--grep=%s", ignoredCommits), "--invert-grep", "--pretty=format:%x1e%h%x1f%aE%x1f%s%x1f%b", "--abbrev-commit", prevTag+".."+ref)
+ if err != nil {
+ return ",", err
+ }
+
+ return log, err
+}
+
+func gitLog() (string, error) {
+ return gitLogBefore("HEAD")
+}
+
+func gitShort(args ...string) (output string, err error) {
+ output, err = git(args...)
+ return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err
+}
diff --git a/releaser/git_test.go b/releaser/git_test.go
new file mode 100644
index 000000000..dc1db5dc7
--- /dev/null
+++ b/releaser/git_test.go
@@ -0,0 +1,53 @@
+// 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 releaser
+
+import (
+ "testing"
+
+ "runtime"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGitInfos(t *testing.T) {
+ if runtime.GOOS == "linux" {
+ // Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328
+ t.Skip("Skip git test on Linux to make Travis happy.")
+ }
+ infos, err := getGitInfos(false)
+
+ require.NoError(t, err)
+ require.True(t, len(infos) > 0)
+
+}
+
+func TestIssuesRe(t *testing.T) {
+
+ body := `
+This is a commit message.
+
+Updates #123
+Fix #345
+closes #543
+See #456
+ `
+
+ issues := extractIssues(body)
+
+ require.Len(t, issues, 4)
+ require.Equal(t, 123, issues[0])
+ require.Equal(t, 543, issues[2])
+
+}
diff --git a/releaser/github.go b/releaser/github.go
new file mode 100644
index 000000000..0dbb1bca1
--- /dev/null
+++ b/releaser/github.go
@@ -0,0 +1,129 @@
+package releaser
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+)
+
+var (
+ gitHubCommitsApi = "https://api.github.com/repos/spf13/hugo/commits/%s"
+ gitHubRepoApi = "https://api.github.com/repos/spf13/hugo"
+ gitHubContributorsApi = "https://api.github.com/repos/spf13/hugo/contributors"
+)
+
+type gitHubCommit struct {
+ Author gitHubAuthor `json:"author"`
+ HtmlURL string `json:"html_url"`
+}
+
+type gitHubAuthor struct {
+ ID int `json:"id"`
+ Login string `json:"login"`
+ HtmlURL string `json:"html_url"`
+ AvatarURL string `json:"avatar_url"`
+}
+
+type gitHubRepo struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ HtmlURL string `json:"html_url"`
+ Stars int `json:"stargazers_count"`
+ Contributors []gitHubContributor
+}
+
+type gitHubContributor struct {
+ ID int `json:"id"`
+ Login string `json:"login"`
+ HtmlURL string `json:"html_url"`
+ Contributions int `json:"contributions"`
+}
+
+func fetchCommit(ref string) (gitHubCommit, error) {
+ var commit gitHubCommit
+
+ u := fmt.Sprintf(gitHubCommitsApi, ref)
+
+ req, err := http.NewRequest("GET", u, nil)
+ if err != nil {
+ return commit, err
+ }
+
+ err = doGitHubRequest(req, &commit)
+
+ return commit, err
+}
+
+func fetchRepo() (gitHubRepo, error) {
+ var repo gitHubRepo
+
+ req, err := http.NewRequest("GET", gitHubRepoApi, nil)
+ if err != nil {
+ return repo, err
+ }
+
+ err = doGitHubRequest(req, &repo)
+ if err != nil {
+ return repo, err
+ }
+
+ var contributors []gitHubContributor
+ page := 0
+ for {
+ page++
+ var currPage []gitHubContributor
+ url := fmt.Sprintf(gitHubContributorsApi+"?page=%d", page)
+
+ req, err = http.NewRequest("GET", url, nil)
+ if err != nil {
+ return repo, err
+ }
+
+ err = doGitHubRequest(req, &currPage)
+ if err != nil {
+ return repo, err
+ }
+ if len(currPage) == 0 {
+ break
+ }
+
+ contributors = append(contributors, currPage...)
+
+ }
+
+ repo.Contributors = contributors
+
+ return repo, err
+
+}
+
+func doGitHubRequest(req *http.Request, v interface{}) error {
+ addGitHubToken(req)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if isError(resp) {
+ b, _ := ioutil.ReadAll(resp.Body)
+ return fmt.Errorf("GitHub lookup failed: %s", string(b))
+ }
+
+ return json.NewDecoder(resp.Body).Decode(v)
+}
+
+func isError(resp *http.Response) bool {
+ return resp.StatusCode < 200 || resp.StatusCode > 299
+}
+
+func addGitHubToken(req *http.Request) {
+ gitHubToken := os.Getenv("GITHUB_TOKEN")
+ if gitHubToken != "" {
+ req.Header.Add("Authorization", "token "+gitHubToken)
+ }
+}
diff --git a/releaser/github_test.go b/releaser/github_test.go
new file mode 100644
index 000000000..7feae75f5
--- /dev/null
+++ b/releaser/github_test.go
@@ -0,0 +1,42 @@
+// 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 releaser
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestGitHubLookupCommit(t *testing.T) {
+ skipIfNoToken(t)
+ commit, err := fetchCommit("793554108763c0984f1a1b1a6ee5744b560d78d0")
+ require.NoError(t, err)
+ fmt.Println(commit)
+}
+
+func TestFetchRepo(t *testing.T) {
+ skipIfNoToken(t)
+ repo, err := fetchRepo()
+ require.NoError(t, err)
+ fmt.Println(">>", len(repo.Contributors))
+}
+
+func skipIfNoToken(t *testing.T) {
+ if os.Getenv("GITHUB_TOKEN") == "" {
+ t.Skip("Skip test against GitHub as no GITHUB_TOKEN set.")
+ }
+}
diff --git a/releaser/releasenotes_writer.go b/releaser/releasenotes_writer.go
new file mode 100644
index 000000000..3d48d9ae8
--- /dev/null
+++ b/releaser/releasenotes_writer.go
@@ -0,0 +1,245 @@
+// 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 release implements a set of utilities and a wrapper around Goreleaser
+// to help automate the Hugo release process.
+package releaser
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "text/template"
+ "time"
+)
+
+const (
+ issueLinkTemplate = "[#%d](https://github.com/spf13/hugo/issues/%d)"
+ linkTemplate = "[%s](%s)"
+ releaseNotesMarkdownTemplate = `
+{{- $patchRelease := isPatch . -}}
+{{- $contribsPerAuthor := .All.ContribCountPerAuthor -}}
+
+{{- if $patchRelease }}
+{{ if eq (len .All) 1 }}
+This is a bug-fix release with one important fix.
+{{ else }}
+This is a bug-fix relase with a couple of important fixes.
+{{ end }}
+{{ else }}
+This release represents **{{ len .All }} contributions by {{ len $contribsPerAuthor }} contributors** to the main Hugo code base.
+{{ end -}}
+
+{{- if gt (len $contribsPerAuthor) 3 -}}
+{{- $u1 := index $contribsPerAuthor 0 -}}
+{{- $u2 := index $contribsPerAuthor 1 -}}
+{{- $u3 := index $contribsPerAuthor 2 -}}
+{{- $u4 := index $contribsPerAuthor 3 -}}
+{{- $u1.AuthorLink }} leads the Hugo development with a significant amount of contributions, but also a big shoutout to {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their ongoing contributions.
+And as always a big thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his relentless work on keeping the documentation and the themes site in pristine condition.
+{{ end }}
+Hugo now has:
+
+{{ with .Repo -}}
+* {{ .Stars }}+ [stars](https://github.com/spf13/hugo/stargazers)
+* {{ len .Contributors }}+ [contributors](https://github.com/spf13/hugo/graphs/contributors)
+{{- end -}}
+{{ with .ThemeCount }}
+* 156+ [themes](http://themes.gohugo.io/)
+{{- end }}
+
+## Enhancements
+{{ template "change-headers" .Enhancements -}}
+## Fixes
+{{ template "change-headers" .Fixes -}}
+
+{{ define "change-headers" }}
+{{ $tmplChanges := index . "templateChanges" -}}
+{{- $outChanges := index . "outChanges" -}}
+{{- $coreChanges := index . "coreChanges" -}}
+{{- $docsChanges := index . "docsChanges" -}}
+{{- $otherChanges := index . "otherChanges" -}}
+{{- with $tmplChanges -}}
+### Templates
+{{ template "change-section" . }}
+{{- end -}}
+{{- with $outChanges -}}
+### Output
+{{- template "change-section" . }}
+{{- end -}}
+{{- with $coreChanges -}}
+### Core
+{{ template "change-section" . }}
+{{- end -}}
+{{- with $docsChanges -}}
+### Docs
+{{- template "change-section" . }}
+{{- end -}}
+{{- with $otherChanges -}}
+### Other
+{{ template "change-section" . }}
+{{- end -}}
+{{ end }}
+
+
+{{ define "change-section" }}
+{{ range . }}
+{{- if .GitHubCommit -}}
+* {{ .Subject }} {{ . | commitURL }} {{ . | authorURL }} {{ range .Issues }}{{ . | issue }} {{ end }}
+{{ else -}}
+* {{ .Subject }} {{ range .Issues }}{{ . | issue }} {{ end }}
+{{ end -}}
+{{- end }}
+{{ end }}
+`
+)
+
+var templateFuncs = template.FuncMap{
+ "isPatch": func(c changeLog) bool {
+ return strings.Count(c.Version, ".") > 1
+ },
+ "issue": func(id int) string {
+ return fmt.Sprintf(issueLinkTemplate, id, id)
+ },
+ "commitURL": func(info gitInfo) string {
+ if info.GitHubCommit.HtmlURL == "" {
+ return ""
+ }
+ return fmt.Sprintf(linkTemplate, info.Hash, info.GitHubCommit.HtmlURL)
+ },
+ "authorURL": func(info gitInfo) string {
+ if info.GitHubCommit.Author.Login == "" {
+ return ""
+ }
+ return fmt.Sprintf(linkTemplate, "@"+info.GitHubCommit.Author.Login, info.GitHubCommit.Author.HtmlURL)
+ },
+}
+
+func writeReleaseNotes(version string, infos gitInfos, to io.Writer) error {
+ changes := gitInfosToChangeLog(infos)
+ changes.Version = version
+ repo, err := fetchRepo()
+ if err == nil {
+ changes.Repo = &repo
+ }
+ themeCount, err := fetchThemeCount()
+ if err == nil {
+ changes.ThemeCount = themeCount
+ }
+
+ tmpl, err := template.New("").Funcs(templateFuncs).Parse(releaseNotesMarkdownTemplate)
+ if err != nil {
+ return err
+ }
+
+ err = tmpl.Execute(to, changes)
+ if err != nil {
+ return err
+ }
+
+ return nil
+
+}
+
+func fetchThemeCount() (int, error) {
+ resp, err := http.Get("https://github.com/spf13/hugoThemes/blob/master/.gitmodules")
+ if err != nil {
+ return 0, err
+ }
+ defer resp.Body.Close()
+
+ b, _ := ioutil.ReadAll(resp.Body)
+ return bytes.Count(b, []byte("submodule")), nil
+}
+
+func writeReleaseNotesToTmpFile(version string, infos gitInfos) (string, error) {
+ f, err := ioutil.TempFile("", "hugorelease")
+ if err != nil {
+ return "", err
+ }
+
+ defer f.Close()
+
+ if err := writeReleaseNotes(version, infos, f); err != nil {
+ return "", err
+ }
+
+ return f.Name(), nil
+}
+
+func getRelaseNotesDocsTempDirAndName(version string) (string, string) {
+ return hugoFilepath("docs/temp"), fmt.Sprintf("%s-relnotes.md", version)
+}
+
+func getRelaseNotesDocsTempFilename(version string) string {
+ return filepath.Join(getRelaseNotesDocsTempDirAndName(version))
+}
+
+func writeReleaseNotesToDocsTemp(version string, infos gitInfos) (string, error) {
+ docsTempPath, name := getRelaseNotesDocsTempDirAndName(version)
+ os.Mkdir(docsTempPath, os.ModePerm)
+
+ f, err := os.Create(filepath.Join(docsTempPath, name))
+ if err != nil {
+ return "", err
+ }
+
+ defer f.Close()
+
+ if err := writeReleaseNotes(version, infos, f); err != nil {
+ return "", err
+ }
+
+ return f.Name(), nil
+
+}
+
+func writeReleaseNotesToDocs(title, sourceFilename string) (string, error) {
+ targetFilename := filepath.Base(sourceFilename)
+ contentDir := hugoFilepath("docs/content/release-notes")
+ targetFullFilename := filepath.Join(contentDir, targetFilename)
+ os.Mkdir(contentDir, os.ModePerm)
+
+ b, err := ioutil.ReadFile(sourceFilename)
+ if err != nil {
+ return "", err
+ }
+
+ f, err := os.Create(targetFullFilename)
+ if err != nil {
+ return "", err
+ }
+ defer f.Close()
+
+ if _, err := f.WriteString(fmt.Sprintf(`
+---
+date: %s
+title: %s
+---
+
+ `, time.Now().Format("2006-01-02"), title)); err != nil {
+ return "", err
+ }
+
+ if _, err := f.Write(b); err != nil {
+ return "", err
+ }
+
+ return targetFullFilename, nil
+
+}
diff --git a/releaser/releasenotes_writer_test.go b/releaser/releasenotes_writer_test.go
new file mode 100644
index 000000000..d1151bffc
--- /dev/null
+++ b/releaser/releasenotes_writer_test.go
@@ -0,0 +1,44 @@
+// 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 defines and implements command-line commands and flags
+// used by Hugo. Commands and flags are implemented using Cobra.
+
+package releaser
+
+import (
+ "bytes"
+ "fmt"
+ "testing"
+
+ "runtime"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestReleaseNotesWriter(t *testing.T) {
+ if runtime.GOOS == "linux" {
+ // Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328
+ t.Skip("Skip git test on Linux to make Travis happy.")
+ }
+
+ var b bytes.Buffer
+
+ // TODO(bep) consider to query GitHub directly for the gitlog with author info, probably faster.
+ infos, err := getGitInfosBefore("v0.20", false)
+ require.NoError(t, err)
+
+ require.NoError(t, writeReleaseNotes("0.20", infos, &b))
+
+ fmt.Println(">>>", b.String())
+}
diff --git a/releaser/releaser.go b/releaser/releaser.go
new file mode 100644
index 000000000..b6e9bfde1
--- /dev/null
+++ b/releaser/releaser.go
@@ -0,0 +1,267 @@
+// 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 releaser implements a set of utilities and a wrapper around Goreleaser
+// to help automate the Hugo release process.
+package releaser
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/spf13/hugo/helpers"
+)
+
+const commitPrefix = "releaser:"
+
+type ReleaseHandler struct {
+ patch int
+ step int
+ skipPublish bool
+}
+
+func (r ReleaseHandler) shouldRelease() bool {
+ return r.step < 1 || r.shouldContinue()
+}
+
+func (r ReleaseHandler) shouldContinue() bool {
+ return r.step == 2
+}
+
+func (r ReleaseHandler) shouldPrepare() bool {
+ return r.step < 1 || r.step == 1
+}
+
+func (r ReleaseHandler) calculateVersions(current helpers.HugoVersion) (helpers.HugoVersion, helpers.HugoVersion) {
+ var (
+ newVersion = current
+ finalVersion = current
+ )
+
+ newVersion.Suffix = ""
+
+ if r.shouldContinue() {
+ // The version in the current code base is in the state we want for
+ // the release.
+ if r.patch == 0 {
+ finalVersion = newVersion.Next()
+ }
+ } else if r.patch > 0 {
+ newVersion = helpers.CurrentHugoVersion.NextPatchLevel(r.patch)
+ } else {
+ finalVersion = newVersion.Next()
+ }
+
+ finalVersion.Suffix = "-DEV"
+
+ return newVersion, finalVersion
+}
+
+func New(patch, step int, skipPublish bool) *ReleaseHandler {
+ return &ReleaseHandler{patch: patch, step: step, skipPublish: skipPublish}
+}
+
+func (r *ReleaseHandler) Run() error {
+ if os.Getenv("GITHUB_TOKEN") == "" {
+ return errors.New("GITHUB_TOKEN not set, create one here with the repo scope selected: https://github.com/settings/tokens/new")
+ }
+
+ newVersion, finalVersion := r.calculateVersions(helpers.CurrentHugoVersion)
+
+ version := newVersion.String()
+ tag := "v" + version
+
+ // Exit early if tag already exists
+ out, err := git("tag", "-l", tag)
+
+ if err != nil {
+ return err
+ }
+
+ if strings.Contains(out, tag) {
+ return fmt.Errorf("Tag %q already exists", tag)
+ }
+
+ var gitCommits gitInfos
+
+ if r.shouldPrepare() || r.shouldRelease() {
+ gitCommits, err = getGitInfos(true)
+ if err != nil {
+ return err
+ }
+ }
+
+ if r.shouldPrepare() {
+ if err := bumpVersions(newVersion); err != nil {
+ return err
+ }
+
+ if _, err := git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
+ return err
+ }
+
+ releaseNotesFile, err := writeReleaseNotesToDocsTemp(version, gitCommits)
+ if err != nil {
+ return err
+ }
+
+ if _, err := git("add", releaseNotesFile); err != nil {
+ return err
+ }
+ if _, err := git("commit", "-m", fmt.Sprintf("%s Add relase notes draft for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
+ return err
+ }
+ }
+
+ if !r.shouldRelease() {
+ fmt.Println("Skip release ... Use --state=2 to continue.")
+ return nil
+ }
+
+ releaseNotesFile := getRelaseNotesDocsTempFilename(version)
+
+ // Write the release notes to the docs site as well.
+ docFile, err := writeReleaseNotesToDocs(version, releaseNotesFile)
+ if err != nil {
+ return err
+ }
+
+ if _, err := git("add", docFile); err != nil {
+ return err
+ }
+ if _, err := git("commit", "-m", fmt.Sprintf("%s Add relase notes to /docs for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
+ return err
+ }
+
+ if _, err := git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s [ci deploy]", commitPrefix, newVersion)); err != nil {
+ return err
+ }
+
+ if err := r.release(releaseNotesFile); err != nil {
+ return err
+ }
+
+ if err := bumpVersions(finalVersion); err != nil {
+ return err
+ }
+
+ // No longer needed.
+ if err := os.Remove(releaseNotesFile); err != nil {
+ return err
+ }
+
+ if _, err := git("commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *ReleaseHandler) release(releaseNotesFile string) error {
+ cmd := exec.Command("goreleaser", "--release-notes", releaseNotesFile, "--skip-publish="+fmt.Sprint(r.skipPublish))
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err := cmd.Run()
+ if err != nil {
+ return fmt.Errorf("goreleaser failed: %s", err)
+ }
+ return nil
+}
+
+func bumpVersions(ver helpers.HugoVersion) error {
+ fromDev := ""
+ toDev := ""
+
+ if ver.Suffix != "" {
+ toDev = "-DEV"
+ } else {
+ fromDev = "-DEV"
+ }
+
+ if err := replaceInFile("helpers/hugo.go",
+ `Number:(\s{4,})(.*),`, fmt.Sprintf(`Number:${1}%.2f,`, ver.Number),
+ `PatchLevel:(\s*)(.*),`, fmt.Sprintf(`PatchLevel:${1}%d,`, ver.PatchLevel),
+ fmt.Sprintf(`Suffix:(\s{4,})"%s",`, fromDev), fmt.Sprintf(`Suffix:${1}"%s",`, toDev)); err != nil {
+ return err
+ }
+
+ snapcraftGrade := "stable"
+ if ver.Suffix != "" {
+ snapcraftGrade = "devel"
+ }
+ if err := replaceInFile("snapcraft.yaml",
+ `version: "(.*)"`, fmt.Sprintf(`version: "%s"`, ver),
+ `grade: (.*) #`, fmt.Sprintf(`grade: %s #`, snapcraftGrade)); err != nil {
+ return err
+ }
+
+ var minVersion string
+ if ver.Suffix != "" {
+ // People use the DEV version in daily use, and we cannot create new themes
+ // with the next version before it is released.
+ minVersion = ver.Prev().String()
+ } else {
+ minVersion = ver.String()
+ }
+
+ if err := replaceInFile("commands/new.go",
+ `min_version = "(.*)"`, fmt.Sprintf(`min_version = "%s"`, minVersion)); err != nil {
+ return err
+ }
+
+ // docs/config.toml
+ if err := replaceInFile("docs/config.toml",
+ `release = "(.*)"`, fmt.Sprintf(`release = "%s"`, ver)); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func replaceInFile(filename string, oldNew ...string) error {
+ fullFilename := hugoFilepath(filename)
+ fi, err := os.Stat(fullFilename)
+ if err != nil {
+ return err
+ }
+
+ b, err := ioutil.ReadFile(fullFilename)
+ if err != nil {
+ return err
+ }
+ newContent := string(b)
+
+ for i := 0; i < len(oldNew); i += 2 {
+ re := regexp.MustCompile(oldNew[i])
+ newContent = re.ReplaceAllString(newContent, oldNew[i+1])
+ }
+
+ return ioutil.WriteFile(fullFilename, []byte(newContent), fi.Mode())
+
+ return nil
+}
+
+func hugoFilepath(filename string) string {
+ pwd, err := os.Getwd()
+ if err != nil {
+ log.Fatal(err)
+ }
+ return filepath.Join(pwd, filename)
+}
diff --git a/releaser/releaser_test.go b/releaser/releaser_test.go
new file mode 100644
index 000000000..641600545
--- /dev/null
+++ b/releaser/releaser_test.go
@@ -0,0 +1,78 @@
+// 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 defines and implements command-line commands and flags
+// used by Hugo. Commands and flags are implemented using Cobra.
+
+package releaser
+
+import (
+ "testing"
+
+ "github.com/spf13/hugo/helpers"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCalculateVersions(t *testing.T) {
+ startVersion := helpers.HugoVersion{Number: 0.20, Suffix: "-DEV"}
+
+ tests := []struct {
+ handler *ReleaseHandler
+ version helpers.HugoVersion
+ v1 string
+ v2 string
+ }{
+ {
+ New(0, 0, true),
+ startVersion,
+ "0.20",
+ "0.21-DEV",
+ },
+ {
+ New(2, 0, true),
+ startVersion,
+ "0.20.2",
+ "0.20-DEV",
+ },
+ {
+ New(0, 1, true),
+ startVersion,
+ "0.20",
+ "0.21-DEV",
+ },
+ {
+ New(0, 2, true),
+ startVersion,
+ "0.20",
+ "0.21-DEV",
+ },
+ {
+ New(3, 1, true),
+ startVersion,
+ "0.20.3",
+ "0.20-DEV",
+ },
+ {
+ New(3, 2, true),
+ startVersion.Next(),
+ "0.21",
+ "0.21-DEV",
+ },
+ }
+
+ for _, test := range tests {
+ v1, v2 := test.handler.calculateVersions(test.version)
+ require.Equal(t, test.v1, v1.String(), "Release version")
+ require.Equal(t, test.v2, v2.String(), "Final version")
+ }
+}
diff --git a/transform/hugogeneratorinject.go b/transform/hugogeneratorinject.go
index 4c37a058d..d80cdaf26 100644
--- a/transform/hugogeneratorinject.go
+++ b/transform/hugogeneratorinject.go
@@ -22,7 +22,7 @@ import (
)
var metaTagsCheck = regexp.MustCompile(`(?i)`, helpers.HugoVersion())
+var hugoGeneratorTag = fmt.Sprintf(``, helpers.CurrentHugoVersion)
// HugoGeneratorInject injects a meta generator tag for Hugo if none present.
func HugoGeneratorInject(ct contentTransformer) {