mirror of
https://github.com/gohugoio/hugo.git
synced 2025-09-08 23:40:40 +02:00
253
releaser/git.go
253
releaser/git.go
@@ -1,253 +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 releaser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
)
|
||||
|
||||
var issueRe = regexp.MustCompile(`(?i)(?:Updates?|Closes?|Fix.*|See) #(\d+)`)
|
||||
|
||||
type changeLog struct {
|
||||
Version string
|
||||
Notes gitInfos
|
||||
All gitInfos
|
||||
Docs gitInfos
|
||||
|
||||
// Overall stats
|
||||
Repo *gitHubRepo
|
||||
ContributorCount int
|
||||
ThemeCount int
|
||||
}
|
||||
|
||||
func newChangeLog(infos, docInfos gitInfos) *changeLog {
|
||||
log := &changeLog{
|
||||
Docs: docInfos,
|
||||
}
|
||||
|
||||
for _, info := range infos {
|
||||
// TODO(bep) improve
|
||||
if regexp.MustCompile("(?i)deprecate|note").MatchString(info.Subject) {
|
||||
log.Notes = append(log.Notes, info)
|
||||
}
|
||||
|
||||
log.All = append(log.All, info)
|
||||
info.Subject = strings.TrimSpace(info.Subject)
|
||||
|
||||
}
|
||||
|
||||
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, _ := hexec.SafeCommand("git", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func getGitInfos(tag, repo, repoPath string, remote bool) (gitInfos, error) {
|
||||
return getGitInfosBefore("HEAD", tag, repo, repoPath, 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, tag, repo, repoPath string, remote bool) (gitInfos, error) {
|
||||
client := newGitHubAPI(repo)
|
||||
var g gitInfos
|
||||
|
||||
log, err := gitLogBefore(ref, tag, repoPath)
|
||||
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{}
|
||||
|
||||
if len(items) > 0 {
|
||||
gi.Hash = items[0]
|
||||
}
|
||||
if len(items) > 1 {
|
||||
gi.Author = items[1]
|
||||
}
|
||||
if len(items) > 2 {
|
||||
gi.Subject = items[2]
|
||||
}
|
||||
if len(items) > 3 {
|
||||
gi.Body = items[3]
|
||||
}
|
||||
|
||||
if remote && gi.Hash != "" {
|
||||
gc, err := client.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 = "snapcraft:|Merge commit|Squashed"
|
||||
|
||||
func gitLogBefore(ref, tag, repoPath string) (string, error) {
|
||||
var prevTag string
|
||||
var err error
|
||||
if tag != "" {
|
||||
prevTag = tag
|
||||
} else {
|
||||
prevTag, err = gitVersionTagBefore(ref)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
defaultArgs := []string{"log", "-E", fmt.Sprintf("--grep=%s", ignoredCommits), "--invert-grep", "--pretty=format:%x1e%h%x1f%aE%x1f%s%x1f%b", "--abbrev-commit", prevTag + ".." + ref}
|
||||
|
||||
var args []string
|
||||
|
||||
if repoPath != "" {
|
||||
args = append([]string{"-C", repoPath}, defaultArgs...)
|
||||
} else {
|
||||
args = defaultArgs
|
||||
}
|
||||
|
||||
log, err := git(args...)
|
||||
if err != nil {
|
||||
return ",", err
|
||||
}
|
||||
|
||||
return log, err
|
||||
}
|
||||
|
||||
func gitVersionTagBefore(ref string) (string, error) {
|
||||
return gitShort("describe", "--tags", "--abbrev=0", "--always", "--match", "v[0-9]*", ref+"^")
|
||||
}
|
||||
|
||||
func gitShort(args ...string) (output string, err error) {
|
||||
output, err = git(args...)
|
||||
return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err
|
||||
}
|
||||
|
||||
func tagExists(tag string) (bool, error) {
|
||||
out, err := git("tag", "-l", tag)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.Contains(out, tag) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
@@ -1,86 +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 releaser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestGitInfos(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
skipIfCI(t)
|
||||
infos, err := getGitInfos("v0.20", "hugo", "", false)
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(len(infos) > 0, qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestIssuesRe(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
body := `
|
||||
This is a commit message.
|
||||
|
||||
Updates #123
|
||||
Fix #345
|
||||
closes #543
|
||||
See #456
|
||||
`
|
||||
|
||||
issues := extractIssues(body)
|
||||
|
||||
c.Assert(len(issues), qt.Equals, 4)
|
||||
c.Assert(issues[0], qt.Equals, 123)
|
||||
c.Assert(issues[2], qt.Equals, 543)
|
||||
|
||||
bodyNoIssues := `
|
||||
This is a commit message without issue refs.
|
||||
|
||||
But it has e #10 to make old regexp confused.
|
||||
Streets #20.
|
||||
`
|
||||
|
||||
emptyIssuesList := extractIssues(bodyNoIssues)
|
||||
c.Assert(len(emptyIssuesList), qt.Equals, 0)
|
||||
}
|
||||
|
||||
func TestGitVersionTagBefore(t *testing.T) {
|
||||
skipIfCI(t)
|
||||
c := qt.New(t)
|
||||
v1, err := gitVersionTagBefore("v0.18")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v1, qt.Equals, "v0.17")
|
||||
}
|
||||
|
||||
func TestTagExists(t *testing.T) {
|
||||
skipIfCI(t)
|
||||
c := qt.New(t)
|
||||
b1, err := tagExists("v0.18")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b1, qt.Equals, true)
|
||||
|
||||
b2, err := tagExists("adfagdsfg")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b2, qt.Equals, false)
|
||||
}
|
||||
|
||||
func skipIfCI(t *testing.T) {
|
||||
if isCI() {
|
||||
// Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328
|
||||
// Also Travis clones very shallowly, making some of the tests above shaky.
|
||||
t.Skip("Skip git test on Linux to make Travis happy.")
|
||||
}
|
||||
}
|
@@ -1,143 +0,0 @@
|
||||
package releaser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
gitHubCommitsAPI = "https://api.github.com/repos/gohugoio/REPO/commits/%s"
|
||||
gitHubRepoAPI = "https://api.github.com/repos/gohugoio/REPO"
|
||||
gitHubContributorsAPI = "https://api.github.com/repos/gohugoio/REPO/contributors"
|
||||
)
|
||||
|
||||
type gitHubAPI struct {
|
||||
commitsAPITemplate string
|
||||
repoAPI string
|
||||
contributorsAPITemplate string
|
||||
}
|
||||
|
||||
func newGitHubAPI(repo string) *gitHubAPI {
|
||||
return &gitHubAPI{
|
||||
commitsAPITemplate: strings.Replace(gitHubCommitsAPI, "REPO", repo, -1),
|
||||
repoAPI: strings.Replace(gitHubRepoAPI, "REPO", repo, -1),
|
||||
contributorsAPITemplate: strings.Replace(gitHubContributorsAPI, "REPO", repo, -1),
|
||||
}
|
||||
}
|
||||
|
||||
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 (g *gitHubAPI) fetchCommit(ref string) (gitHubCommit, error) {
|
||||
var commit gitHubCommit
|
||||
|
||||
u := fmt.Sprintf(g.commitsAPITemplate, ref)
|
||||
|
||||
req, err := http.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return commit, err
|
||||
}
|
||||
|
||||
err = doGitHubRequest(req, &commit)
|
||||
|
||||
return commit, err
|
||||
}
|
||||
|
||||
func (g *gitHubAPI) fetchRepo() (gitHubRepo, error) {
|
||||
var repo gitHubRepo
|
||||
|
||||
req, err := http.NewRequest("GET", g.repoAPI, 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(g.contributorsAPITemplate+"?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 any) 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)
|
||||
}
|
||||
}
|
@@ -1,46 +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 releaser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestGitHubLookupCommit(t *testing.T) {
|
||||
skipIfNoToken(t)
|
||||
c := qt.New(t)
|
||||
client := newGitHubAPI("hugo")
|
||||
commit, err := client.fetchCommit("793554108763c0984f1a1b1a6ee5744b560d78d0")
|
||||
c.Assert(err, qt.IsNil)
|
||||
fmt.Println(commit)
|
||||
}
|
||||
|
||||
func TestFetchRepo(t *testing.T) {
|
||||
skipIfNoToken(t)
|
||||
c := qt.New(t)
|
||||
client := newGitHubAPI("hugo")
|
||||
repo, err := client.fetchRepo()
|
||||
c.Assert(err, qt.IsNil)
|
||||
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.")
|
||||
}
|
||||
}
|
@@ -1,191 +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 releaser 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"
|
||||
)
|
||||
|
||||
const (
|
||||
issueLinkTemplate = "#%d"
|
||||
linkTemplate = "[%s](%s)"
|
||||
releaseNotesMarkdownTemplatePatchRelease = `
|
||||
{{ if eq (len .All) 1 }}
|
||||
This is a bug-fix release with one important fix.
|
||||
{{ else }}
|
||||
This is a bug-fix release with a couple of important fixes.
|
||||
{{ end }}
|
||||
{{ range .All }}
|
||||
{{- if .GitHubCommit -}}
|
||||
* {{ .Subject }} {{ .Hash }} {{ . | author }} {{ range .Issues }}{{ . | issue }} {{ end }}
|
||||
{{ else -}}
|
||||
* {{ .Subject }} {{ range .Issues }}{{ . | issue }} {{ end }}
|
||||
{{ end -}}
|
||||
{{- end }}
|
||||
|
||||
|
||||
`
|
||||
releaseNotesMarkdownTemplate = `
|
||||
{{- $contribsPerAuthor := .All.ContribCountPerAuthor -}}
|
||||
{{- $docsContribsPerAuthor := .Docs.ContribCountPerAuthor -}}
|
||||
|
||||
This release represents **{{ len .All }} contributions by {{ len $contribsPerAuthor }} contributors** to the main Hugo code base.
|
||||
|
||||
{{- 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 thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his ongoing work on keeping the themes site in pristine condition.
|
||||
{{ end }}
|
||||
Many have also been busy writing and fixing the documentation in [hugoDocs](https://github.com/gohugoio/hugoDocs),
|
||||
which has received **{{ len .Docs }} contributions by {{ len $docsContribsPerAuthor }} contributors**.
|
||||
{{- if gt (len $docsContribsPerAuthor) 3 -}}
|
||||
{{- $u1 := index $docsContribsPerAuthor 0 -}}
|
||||
{{- $u2 := index $docsContribsPerAuthor 1 -}}
|
||||
{{- $u3 := index $docsContribsPerAuthor 2 -}}
|
||||
{{- $u4 := index $docsContribsPerAuthor 3 }} A special thanks to {{ $u1.AuthorLink }}, {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their work on the documentation site.
|
||||
{{ end }}
|
||||
|
||||
Hugo now has:
|
||||
|
||||
{{ with .Repo -}}
|
||||
* {{ .Stars }}+ [stars](https://github.com/gohugoio/hugo/stargazers)
|
||||
* {{ len .Contributors }}+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors)
|
||||
{{- end -}}
|
||||
{{ with .ThemeCount }}
|
||||
* {{ . }}+ [themes](http://themes.gohugo.io/)
|
||||
{{ end }}
|
||||
{{ with .Notes }}
|
||||
## Notes
|
||||
{{ template "change-section" . }}
|
||||
{{- end -}}
|
||||
{{ with .All }}
|
||||
## Changes
|
||||
{{ template "change-section" . }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "change-section" }}
|
||||
{{ range . }}
|
||||
{{- if .GitHubCommit -}}
|
||||
* {{ .Subject }} {{ .Hash }} {{ . | author }} {{ range .Issues }}{{ . | issue }} {{ end }}
|
||||
{{ else -}}
|
||||
* {{ .Subject }} {{ range .Issues }}{{ . | issue }} {{ end }}
|
||||
{{ end -}}
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
`
|
||||
)
|
||||
|
||||
var templateFuncs = template.FuncMap{
|
||||
"isPatch": func(c changeLog) bool {
|
||||
return !strings.HasSuffix(c.Version, "0")
|
||||
},
|
||||
"issue": func(id int) string {
|
||||
return fmt.Sprintf(issueLinkTemplate, id)
|
||||
},
|
||||
"commitURL": func(info gitInfo) string {
|
||||
if info.GitHubCommit.HTMLURL == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(linkTemplate, info.Hash, info.GitHubCommit.HTMLURL)
|
||||
},
|
||||
"author": func(info gitInfo) string {
|
||||
return "@" + info.GitHubCommit.Author.Login
|
||||
},
|
||||
}
|
||||
|
||||
func writeReleaseNotes(version string, infosMain, infosDocs gitInfos, to io.Writer) error {
|
||||
client := newGitHubAPI("hugo")
|
||||
changes := newChangeLog(infosMain, infosDocs)
|
||||
changes.Version = version
|
||||
repo, err := client.fetchRepo()
|
||||
if err == nil {
|
||||
changes.Repo = &repo
|
||||
}
|
||||
themeCount, err := fetchThemeCount()
|
||||
if err == nil {
|
||||
changes.ThemeCount = themeCount
|
||||
}
|
||||
|
||||
mtempl := releaseNotesMarkdownTemplate
|
||||
|
||||
if !strings.HasSuffix(version, "0") {
|
||||
mtempl = releaseNotesMarkdownTemplatePatchRelease
|
||||
}
|
||||
|
||||
tmpl, err := template.New("").Funcs(templateFuncs).Parse(mtempl)
|
||||
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://raw.githubusercontent.com/gohugoio/hugoThemesSiteBuilder/main/themes.txt")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
return bytes.Count(b, []byte("\n")) - bytes.Count(b, []byte("#")), nil
|
||||
}
|
||||
|
||||
func getReleaseNotesFilename(version string) string {
|
||||
return filepath.FromSlash(fmt.Sprintf("temp/%s-relnotes-ready.md", version))
|
||||
}
|
||||
|
||||
func (r *ReleaseHandler) writeReleaseNotesToTemp(version string, isPatch bool, infosMain, infosDocs gitInfos) (string, error) {
|
||||
filename := getReleaseNotesFilename(version)
|
||||
|
||||
var w io.WriteCloser
|
||||
|
||||
if !r.try {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
w = f
|
||||
|
||||
} else {
|
||||
w = os.Stdout
|
||||
}
|
||||
|
||||
if err := writeReleaseNotes(version, infosMain, infosDocs, w); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
@@ -1,46 +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 defines and implements command-line commands and flags
|
||||
// used by Hugo. Commands and flags are implemented using Cobra.
|
||||
|
||||
package releaser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func _TestReleaseNotesWriter(t *testing.T) {
|
||||
skipIfNoToken(t)
|
||||
if os.Getenv("CI") != "" {
|
||||
// Travis has an ancient git with no --invert-grep: https://github.com/travis-ci/travis-ci/issues/6328
|
||||
t.Skip("Skip git test on CI to make Travis happy..")
|
||||
}
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
// TODO(bep) consider to query GitHub directly for the gitlog with author info, probably faster.
|
||||
infos, err := getGitInfosBefore("HEAD", "v0.89.0", "hugo", "", false)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(writeReleaseNotes("0.89.0", infos, infos, &b), qt.IsNil)
|
||||
|
||||
fmt.Println(b.String())
|
||||
}
|
@@ -17,7 +17,6 @@ package releaser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -25,22 +24,56 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
)
|
||||
|
||||
const commitPrefix = "releaser:"
|
||||
|
||||
// New initialises a ReleaseHandler.
|
||||
func New(skipPush, try bool, step int) (*ReleaseHandler, error) {
|
||||
if step < 1 || step > 2 {
|
||||
return nil, fmt.Errorf("step must be 1 or 2")
|
||||
}
|
||||
|
||||
prefix := "release-"
|
||||
branch, err := git("rev-parse", "--abbrev-ref", "HEAD")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(branch, prefix) {
|
||||
return nil, fmt.Errorf("branch %q is not a release branch", branch)
|
||||
}
|
||||
|
||||
logf("Branch: %s\n", branch)
|
||||
|
||||
version := strings.TrimPrefix(branch, prefix)
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
rh := &ReleaseHandler{branchVersion: version, skipPush: skipPush, try: try, step: step}
|
||||
|
||||
if try {
|
||||
rh.git = func(args ...string) (string, error) {
|
||||
logln("git", strings.Join(args, " "))
|
||||
return "", nil
|
||||
}
|
||||
} else {
|
||||
rh.git = git
|
||||
}
|
||||
|
||||
return rh, nil
|
||||
}
|
||||
|
||||
// ReleaseHandler provides functionality to release a new version of Hugo.
|
||||
// Test this locally without doing an actual release:
|
||||
// go run -tags release main.go release --skip-publish --try -r 0.90.0
|
||||
// Or a variation of the above -- the skip-publish flag makes sure that any changes are performed to the local Git only.
|
||||
type ReleaseHandler struct {
|
||||
cliVersion string
|
||||
branchVersion string
|
||||
|
||||
skipPublish bool
|
||||
// 1 or 2.
|
||||
step int
|
||||
|
||||
// No remote pushes.
|
||||
skipPush bool
|
||||
|
||||
// Just simulate, no actual changes.
|
||||
try bool
|
||||
@@ -48,144 +81,48 @@ type ReleaseHandler struct {
|
||||
git func(args ...string) (string, error)
|
||||
}
|
||||
|
||||
func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) {
|
||||
newVersion := hugo.MustParseVersion(r.cliVersion)
|
||||
finalVersion := newVersion.Next()
|
||||
finalVersion.PatchLevel = 0
|
||||
|
||||
if newVersion.Suffix != "-test" {
|
||||
newVersion.Suffix = ""
|
||||
}
|
||||
|
||||
finalVersion.Suffix = "-DEV"
|
||||
|
||||
return newVersion, finalVersion
|
||||
}
|
||||
|
||||
// New initialises a ReleaseHandler.
|
||||
func New(version string, skipPublish, try bool) *ReleaseHandler {
|
||||
// When triggered from CI release branch
|
||||
version = strings.TrimPrefix(version, "release-")
|
||||
version = strings.TrimPrefix(version, "v")
|
||||
rh := &ReleaseHandler{cliVersion: version, skipPublish: skipPublish, try: try}
|
||||
|
||||
if try {
|
||||
rh.git = func(args ...string) (string, error) {
|
||||
fmt.Println("git", strings.Join(args, " "))
|
||||
return "", nil
|
||||
}
|
||||
} else {
|
||||
rh.git = git
|
||||
}
|
||||
|
||||
return rh
|
||||
}
|
||||
|
||||
// Run creates a new release.
|
||||
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")
|
||||
}
|
||||
|
||||
fmt.Printf("Start release from %q\n", wd())
|
||||
|
||||
newVersion, finalVersion := r.calculateVersions()
|
||||
|
||||
version := newVersion.String()
|
||||
tag := "v" + version
|
||||
isPatch := newVersion.PatchLevel > 0
|
||||
mainVersion := newVersion
|
||||
mainVersion.PatchLevel = 0
|
||||
|
||||
// Exit early if tag already exists
|
||||
exists, err := tagExists(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.gitPush()
|
||||
|
||||
if exists {
|
||||
return fmt.Errorf("tag %q already exists", tag)
|
||||
}
|
||||
|
||||
var changeLogFromTag string
|
||||
|
||||
if newVersion.PatchLevel == 0 {
|
||||
// There may have been patch releases between, so set the tag explicitly.
|
||||
changeLogFromTag = "v" + newVersion.Prev().String()
|
||||
exists, _ := tagExists(changeLogFromTag)
|
||||
if !exists {
|
||||
// fall back to one that exists.
|
||||
changeLogFromTag = ""
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
gitCommits gitInfos
|
||||
gitCommitsDocs gitInfos
|
||||
)
|
||||
|
||||
defer r.gitPush() // TODO(bep)
|
||||
|
||||
gitCommits, err = getGitInfos(changeLogFromTag, "hugo", "", !r.try)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(bep) explicit tag?
|
||||
gitCommitsDocs, err = getGitInfos("", "hugoDocs", "../hugoDocs", !r.try)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releaseNotesFile, err := r.writeReleaseNotesToTemp(version, isPatch, gitCommits, gitCommitsDocs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := r.git("add", releaseNotesFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commitMsg := fmt.Sprintf("%s Add release notes for %s", commitPrefix, newVersion)
|
||||
commitMsg += "\n[ci skip]"
|
||||
|
||||
if _, err := r.git("commit", "-m", commitMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.bumpVersions(newVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := r.git("tag", "-a", tag, "-m", fmt.Sprintf("%s %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !r.skipPublish {
|
||||
if _, err := r.git("push", "origin", tag); err != nil {
|
||||
if r.step == 1 {
|
||||
if err := r.bumpVersions(newVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.release(releaseNotesFile); err != nil {
|
||||
return err
|
||||
if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Bump versions for release of %s\n\n[ci skip]", commitPrefix, newVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The above commit will be the target for this release, so print it to the console in a env friendly way.
|
||||
sha, err := git("rev-parse", "HEAD")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Hugoreleaser will do the actual release using these values.
|
||||
if err := r.replaceInFile("hugoreleaser.env",
|
||||
`HUGORELEASER_TAG=(\S*)`, "HUGORELEASER_TAG="+tag,
|
||||
`HUGORELEASER_COMMITISH=(\S*)`, "HUGORELEASER_COMMITISH="+sha,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
logf("HUGORELEASER_TAG=%s\n", tag)
|
||||
logf("HUGORELEASER_COMMITISH=%s\n", sha)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := r.bumpVersions(finalVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !r.try {
|
||||
// No longer needed.
|
||||
if err := os.Remove(releaseNotesFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := r.git("commit", "-a", "-m", fmt.Sprintf("%s Prepare repository for %s\n\n[ci skip]", commitPrefix, finalVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -193,36 +130,6 @@ func (r *ReleaseHandler) Run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReleaseHandler) gitPush() {
|
||||
if r.skipPublish {
|
||||
return
|
||||
}
|
||||
if _, err := r.git("push", "origin", "HEAD"); err != nil {
|
||||
log.Fatal("push failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReleaseHandler) release(releaseNotesFile string) error {
|
||||
if r.try {
|
||||
fmt.Println("Skip goreleaser...")
|
||||
return nil
|
||||
}
|
||||
|
||||
args := []string{"--parallelism", "2", "--timeout", "120m", "--rm-dist", "--release-notes", releaseNotesFile}
|
||||
if r.skipPublish {
|
||||
args = append(args, "--skip-publish")
|
||||
}
|
||||
|
||||
cmd, _ := hexec.SafeCommand("goreleaser", args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("goreleaser failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error {
|
||||
toDev := ""
|
||||
|
||||
@@ -264,6 +171,29 @@ func (r *ReleaseHandler) bumpVersions(ver hugo.Version) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r ReleaseHandler) calculateVersions() (hugo.Version, hugo.Version) {
|
||||
newVersion := hugo.MustParseVersion(r.branchVersion)
|
||||
finalVersion := newVersion.Next()
|
||||
finalVersion.PatchLevel = 0
|
||||
|
||||
if newVersion.Suffix != "-test" {
|
||||
newVersion.Suffix = ""
|
||||
}
|
||||
|
||||
finalVersion.Suffix = "-DEV"
|
||||
|
||||
return newVersion, finalVersion
|
||||
}
|
||||
|
||||
func (r *ReleaseHandler) gitPush() {
|
||||
if r.skipPush {
|
||||
return
|
||||
}
|
||||
if _, err := r.git("push", "origin", "HEAD"); err != nil {
|
||||
log.Fatal("push failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error {
|
||||
filename = filepath.FromSlash(filename)
|
||||
fi, err := os.Stat(filename)
|
||||
@@ -272,11 +202,11 @@ func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error
|
||||
}
|
||||
|
||||
if r.try {
|
||||
fmt.Printf("Replace in %q: %q\n", filename, oldNew)
|
||||
logf("Replace in %q: %q\n", filename, oldNew)
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -287,18 +217,22 @@ func (r *ReleaseHandler) replaceInFile(filename string, oldNew ...string) error
|
||||
newContent = re.ReplaceAllString(newContent, oldNew[i+1])
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(filename, []byte(newContent), fi.Mode())
|
||||
return os.WriteFile(filename, []byte(newContent), fi.Mode())
|
||||
}
|
||||
|
||||
func isCI() bool {
|
||||
return os.Getenv("CI") != ""
|
||||
}
|
||||
|
||||
func wd() string {
|
||||
p, err := os.Getwd()
|
||||
func git(args ...string) (string, error) {
|
||||
cmd, _ := hexec.SafeCommand("git", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args)
|
||||
}
|
||||
return p
|
||||
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func logf(format string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
}
|
||||
|
||||
func logln(args ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, args...)
|
||||
}
|
||||
|
Reference in New Issue
Block a user