github.com/olliephillips/hugo@v0.42.2/releaser/releasenotes_writer.go (about)

     1  // Copyright 2017-present The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  // Package releaser implements a set of utilities and a wrapper around Goreleaser
    15  // to help automate the Hugo release process.
    16  package releaser
    17  
    18  import (
    19  	"bytes"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"net/http"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"text/template"
    28  	"time"
    29  )
    30  
    31  const (
    32  	issueLinkTemplate            = "[#%d](https://github.com/gohugoio/hugo/issues/%d)"
    33  	linkTemplate                 = "[%s](%s)"
    34  	releaseNotesMarkdownTemplate = `
    35  {{- $patchRelease := isPatch . -}}
    36  {{- $contribsPerAuthor := .All.ContribCountPerAuthor -}}
    37  {{- $docsContribsPerAuthor := .Docs.ContribCountPerAuthor -}}
    38  {{- if $patchRelease }}
    39  {{ if eq (len .All) 1 }}
    40  This is a bug-fix release with one important fix.
    41  {{ else }}
    42  This is a bug-fix release with a couple of important fixes.
    43  {{ end }}
    44  {{ else }}
    45  This release represents **{{ len .All }} contributions by {{ len $contribsPerAuthor }} contributors** to the main Hugo code base.
    46  {{ end -}}
    47  
    48  {{- if  gt (len $contribsPerAuthor) 3 -}}
    49  {{- $u1 := index $contribsPerAuthor 0 -}}
    50  {{- $u2 := index $contribsPerAuthor 1 -}}
    51  {{- $u3 := index $contribsPerAuthor 2 -}}
    52  {{- $u4 := index $contribsPerAuthor 3 -}}
    53  {{- $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.
    54  And a big thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his relentless work on keeping the themes site in pristine condition and to [@kaushalmodi](https://github.com/kaushalmodi) for his great work on the documentation site.
    55  {{ end }}
    56  {{- if not $patchRelease }}
    57  Many have also been busy writing and fixing the documentation in [hugoDocs](https://github.com/gohugoio/hugoDocs), 
    58  which has received **{{ len .Docs }} contributions by {{ len $docsContribsPerAuthor }} contributors**.
    59  {{- if  gt (len $docsContribsPerAuthor) 3 -}}
    60  {{- $u1 := index $docsContribsPerAuthor 0 -}}
    61  {{- $u2 := index $docsContribsPerAuthor 1 -}}
    62  {{- $u3 := index $docsContribsPerAuthor 2 -}}
    63  {{- $u4 := index $docsContribsPerAuthor 3 }} A special thanks to {{ $u1.AuthorLink }}, {{ $u2.AuthorLink }}, {{ $u3.AuthorLink }}, and {{ $u4.AuthorLink }} for their work on the documentation site.
    64  {{ end }}
    65  {{ end }}
    66  Hugo now has:
    67  
    68  {{ with .Repo -}}
    69  * {{ .Stars }}+ [stars](https://github.com/gohugoio/hugo/stargazers)
    70  * {{ len .Contributors }}+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors)
    71  {{- end -}}
    72  {{ with .ThemeCount }}
    73  * {{ . }}+ [themes](http://themes.gohugo.io/)
    74  {{ end }}
    75  {{ with .Notes }}
    76  ## Notes
    77  {{ template "change-section" . }}
    78  {{- end -}}
    79  ## Enhancements
    80  {{ template "change-headers"  .Enhancements -}}
    81  ## Fixes
    82  {{ template "change-headers"  .Fixes -}}
    83  
    84  {{ define "change-headers" }}
    85  {{ $tmplChanges := index . "templateChanges" -}}
    86  {{- $outChanges := index . "outChanges" -}}
    87  {{- $coreChanges := index . "coreChanges" -}}
    88  {{- $otherChanges := index . "otherChanges" -}}
    89  {{- with $tmplChanges -}}
    90  ### Templates
    91  {{ template "change-section" . }}
    92  {{- end -}}
    93  {{- with $outChanges -}}
    94  ### Output
    95  {{ template "change-section"  . }}
    96  {{- end -}}
    97  {{- with $coreChanges -}}
    98  ### Core
    99  {{ template "change-section" . }}
   100  {{- end -}}
   101  {{- with $otherChanges -}}
   102  ### Other
   103  {{ template "change-section"  . }}
   104  {{- end -}}
   105  {{ end }}
   106  
   107  
   108  {{ define "change-section" }}
   109  {{ range . }}
   110  {{- if .GitHubCommit -}}
   111  * {{ .Subject }} {{ . | commitURL }} {{ . | authorURL }} {{ range .Issues }}{{ . | issue }}{{ end }}
   112  {{ else -}}
   113  * {{ .Subject }} {{ range .Issues }}{{ . | issue }}{{ end }}
   114  {{ end -}}
   115  {{- end }}
   116  {{ end }}
   117  `
   118  )
   119  
   120  var templateFuncs = template.FuncMap{
   121  	"isPatch": func(c changeLog) bool {
   122  		return strings.Count(c.Version, ".") > 1
   123  	},
   124  	"issue": func(id int) string {
   125  		return fmt.Sprintf(issueLinkTemplate, id, id)
   126  	},
   127  	"commitURL": func(info gitInfo) string {
   128  		if info.GitHubCommit.HtmlURL == "" {
   129  			return ""
   130  		}
   131  		return fmt.Sprintf(linkTemplate, info.Hash, info.GitHubCommit.HtmlURL)
   132  	},
   133  	"authorURL": func(info gitInfo) string {
   134  		if info.GitHubCommit.Author.Login == "" {
   135  			return ""
   136  		}
   137  		return fmt.Sprintf(linkTemplate, "@"+info.GitHubCommit.Author.Login, info.GitHubCommit.Author.HtmlURL)
   138  	},
   139  }
   140  
   141  func writeReleaseNotes(version string, infosMain, infosDocs gitInfos, to io.Writer) error {
   142  	client := newGitHubAPI("hugo")
   143  	changes := gitInfosToChangeLog(infosMain, infosDocs)
   144  	changes.Version = version
   145  	repo, err := client.fetchRepo()
   146  	if err == nil {
   147  		changes.Repo = &repo
   148  	}
   149  	themeCount, err := fetchThemeCount()
   150  	if err == nil {
   151  		changes.ThemeCount = themeCount
   152  	}
   153  
   154  	tmpl, err := template.New("").Funcs(templateFuncs).Parse(releaseNotesMarkdownTemplate)
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	err = tmpl.Execute(to, changes)
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	return nil
   165  
   166  }
   167  
   168  func fetchThemeCount() (int, error) {
   169  	resp, err := http.Get("https://raw.githubusercontent.com/gohugoio/hugoThemes/master/.gitmodules")
   170  	if err != nil {
   171  		return 0, err
   172  	}
   173  	defer resp.Body.Close()
   174  
   175  	b, _ := ioutil.ReadAll(resp.Body)
   176  	return bytes.Count(b, []byte("submodule")), nil
   177  }
   178  
   179  func writeReleaseNotesToTmpFile(version string, infosMain, infosDocs gitInfos) (string, error) {
   180  	f, err := ioutil.TempFile("", "hugorelease")
   181  	if err != nil {
   182  		return "", err
   183  	}
   184  
   185  	defer f.Close()
   186  
   187  	if err := writeReleaseNotes(version, infosMain, infosDocs, f); err != nil {
   188  		return "", err
   189  	}
   190  
   191  	return f.Name(), nil
   192  }
   193  
   194  func getReleaseNotesDocsTempDirAndName(version string, final bool) (string, string) {
   195  	if final {
   196  		return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes-ready.md", version)
   197  	}
   198  	return hugoFilepath("temp"), fmt.Sprintf("%s-relnotes.md", version)
   199  }
   200  
   201  func getReleaseNotesDocsTempFilename(version string, final bool) string {
   202  	return filepath.Join(getReleaseNotesDocsTempDirAndName(version, final))
   203  }
   204  
   205  func (r *ReleaseHandler) releaseNotesState(version string) (releaseNotesState, error) {
   206  	docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, false)
   207  	_, err := os.Stat(filepath.Join(docsTempPath, name))
   208  
   209  	if err == nil {
   210  		return releaseNotesCreated, nil
   211  	}
   212  
   213  	docsTempPath, name = getReleaseNotesDocsTempDirAndName(version, true)
   214  	_, err = os.Stat(filepath.Join(docsTempPath, name))
   215  
   216  	if err == nil {
   217  		return releaseNotesReady, nil
   218  	}
   219  
   220  	if !os.IsNotExist(err) {
   221  		return releaseNotesNone, err
   222  	}
   223  
   224  	return releaseNotesNone, nil
   225  
   226  }
   227  
   228  func (r *ReleaseHandler) writeReleaseNotesToTemp(version string, infosMain, infosDocs gitInfos) (string, error) {
   229  
   230  	docsTempPath, name := getReleaseNotesDocsTempDirAndName(version, false)
   231  
   232  	var (
   233  		w io.WriteCloser
   234  	)
   235  
   236  	if !r.try {
   237  		os.Mkdir(docsTempPath, os.ModePerm)
   238  
   239  		f, err := os.Create(filepath.Join(docsTempPath, name))
   240  		if err != nil {
   241  			return "", err
   242  		}
   243  
   244  		name = f.Name()
   245  
   246  		defer f.Close()
   247  
   248  		w = f
   249  
   250  	} else {
   251  		w = os.Stdout
   252  	}
   253  
   254  	if err := writeReleaseNotes(version, infosMain, infosDocs, w); err != nil {
   255  		return "", err
   256  	}
   257  
   258  	return name, nil
   259  
   260  }
   261  
   262  func (r *ReleaseHandler) writeReleaseNotesToDocs(title, sourceFilename string) (string, error) {
   263  	targetFilename := "index.md"
   264  	bundleDir := strings.TrimSuffix(filepath.Base(sourceFilename), "-ready.md")
   265  	contentDir := hugoFilepath("docs/content/en/news/" + bundleDir)
   266  	targetFullFilename := filepath.Join(contentDir, targetFilename)
   267  
   268  	if r.try {
   269  		fmt.Printf("Write release notes to /docs: Bundle %q Dir: %q\n", bundleDir, contentDir)
   270  		return targetFullFilename, nil
   271  	}
   272  
   273  	if err := os.MkdirAll(contentDir, os.ModePerm); err != nil {
   274  		return "", nil
   275  	}
   276  
   277  	b, err := ioutil.ReadFile(sourceFilename)
   278  	if err != nil {
   279  		return "", err
   280  	}
   281  
   282  	f, err := os.Create(targetFullFilename)
   283  	if err != nil {
   284  		return "", err
   285  	}
   286  	defer f.Close()
   287  
   288  	fmTail := ""
   289  	if strings.Count(title, ".") > 1 {
   290  		// Bug fix release
   291  		fmTail = `
   292  images:
   293  - images/blog/hugo-bug-poster.png
   294  `
   295  	}
   296  
   297  	if _, err := f.WriteString(fmt.Sprintf(`
   298  ---
   299  date: %s
   300  title: %q
   301  description: %q
   302  categories: ["Releases"]%s
   303  ---
   304  
   305  	`, time.Now().Format("2006-01-02"), title, title, fmTail)); err != nil {
   306  		return "", err
   307  	}
   308  
   309  	if _, err := f.Write(b); err != nil {
   310  		return "", err
   311  	}
   312  
   313  	return targetFullFilename, nil
   314  
   315  }