github.com/gohugoio/hugo@v0.88.1/releaser/git.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
    15  
    16  import (
    17  	"fmt"
    18  	"regexp"
    19  	"sort"
    20  	"strconv"
    21  	"strings"
    22  
    23  	"github.com/gohugoio/hugo/common/hexec"
    24  )
    25  
    26  var issueRe = regexp.MustCompile(`(?i)[Updates?|Closes?|Fix.*|See] #(\d+)`)
    27  
    28  const (
    29  	notesChanges    = "notesChanges"
    30  	templateChanges = "templateChanges"
    31  	coreChanges     = "coreChanges"
    32  	outChanges      = "outChanges"
    33  	otherChanges    = "otherChanges"
    34  )
    35  
    36  type changeLog struct {
    37  	Version      string
    38  	Enhancements map[string]gitInfos
    39  	Fixes        map[string]gitInfos
    40  	Notes        gitInfos
    41  	All          gitInfos
    42  	Docs         gitInfos
    43  
    44  	// Overall stats
    45  	Repo             *gitHubRepo
    46  	ContributorCount int
    47  	ThemeCount       int
    48  }
    49  
    50  func newChangeLog(infos, docInfos gitInfos) *changeLog {
    51  	return &changeLog{
    52  		Enhancements: make(map[string]gitInfos),
    53  		Fixes:        make(map[string]gitInfos),
    54  		All:          infos,
    55  		Docs:         docInfos,
    56  	}
    57  }
    58  
    59  func (l *changeLog) addGitInfo(isFix bool, info gitInfo, category string) {
    60  	var (
    61  		infos   gitInfos
    62  		found   bool
    63  		segment map[string]gitInfos
    64  	)
    65  
    66  	if category == notesChanges {
    67  		l.Notes = append(l.Notes, info)
    68  		return
    69  	} else if isFix {
    70  		segment = l.Fixes
    71  	} else {
    72  		segment = l.Enhancements
    73  	}
    74  
    75  	infos, found = segment[category]
    76  	if !found {
    77  		infos = gitInfos{}
    78  	}
    79  
    80  	infos = append(infos, info)
    81  	segment[category] = infos
    82  }
    83  
    84  func gitInfosToChangeLog(infos, docInfos gitInfos) *changeLog {
    85  	log := newChangeLog(infos, docInfos)
    86  	for _, info := range infos {
    87  		los := strings.ToLower(info.Subject)
    88  		isFix := strings.Contains(los, "fix")
    89  		category := otherChanges
    90  
    91  		// TODO(bep) improve
    92  		if regexp.MustCompile("(?i)deprecate").MatchString(los) {
    93  			category = notesChanges
    94  		} else if regexp.MustCompile("(?i)tpl|tplimpl:|layout").MatchString(los) {
    95  			category = templateChanges
    96  		} else if regexp.MustCompile("(?i)hugolib:").MatchString(los) {
    97  			category = coreChanges
    98  		} else if regexp.MustCompile("(?i)out(put)?:|media:|Output|Media").MatchString(los) {
    99  			category = outChanges
   100  		}
   101  
   102  		// Trim package prefix.
   103  		colonIdx := strings.Index(info.Subject, ":")
   104  		if colonIdx != -1 && colonIdx < (len(info.Subject)/2) {
   105  			info.Subject = info.Subject[colonIdx+1:]
   106  		}
   107  
   108  		info.Subject = strings.TrimSpace(info.Subject)
   109  
   110  		log.addGitInfo(isFix, info, category)
   111  	}
   112  
   113  	return log
   114  }
   115  
   116  type gitInfo struct {
   117  	Hash    string
   118  	Author  string
   119  	Subject string
   120  	Body    string
   121  
   122  	GitHubCommit *gitHubCommit
   123  }
   124  
   125  func (g gitInfo) Issues() []int {
   126  	return extractIssues(g.Body)
   127  }
   128  
   129  func (g gitInfo) AuthorID() string {
   130  	if g.GitHubCommit != nil {
   131  		return g.GitHubCommit.Author.Login
   132  	}
   133  	return g.Author
   134  }
   135  
   136  func extractIssues(body string) []int {
   137  	var i []int
   138  	m := issueRe.FindAllStringSubmatch(body, -1)
   139  	for _, mm := range m {
   140  		issueID, err := strconv.Atoi(mm[1])
   141  		if err != nil {
   142  			continue
   143  		}
   144  		i = append(i, issueID)
   145  	}
   146  	return i
   147  }
   148  
   149  type gitInfos []gitInfo
   150  
   151  func git(args ...string) (string, error) {
   152  	cmd, _ := hexec.SafeCommand("git", args...)
   153  	out, err := cmd.CombinedOutput()
   154  	if err != nil {
   155  		return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args)
   156  	}
   157  	return string(out), nil
   158  }
   159  
   160  func getGitInfos(tag, repo, repoPath string, remote bool) (gitInfos, error) {
   161  	return getGitInfosBefore("HEAD", tag, repo, repoPath, remote)
   162  }
   163  
   164  type countribCount struct {
   165  	Author       string
   166  	GitHubAuthor gitHubAuthor
   167  	Count        int
   168  }
   169  
   170  func (c countribCount) AuthorLink() string {
   171  	if c.GitHubAuthor.HTMLURL != "" {
   172  		return fmt.Sprintf("[@%s](%s)", c.GitHubAuthor.Login, c.GitHubAuthor.HTMLURL)
   173  	}
   174  
   175  	if !strings.Contains(c.Author, "@") {
   176  		return c.Author
   177  	}
   178  
   179  	return c.Author[:strings.Index(c.Author, "@")]
   180  }
   181  
   182  type contribCounts []countribCount
   183  
   184  func (c contribCounts) Less(i, j int) bool { return c[i].Count > c[j].Count }
   185  func (c contribCounts) Len() int           { return len(c) }
   186  func (c contribCounts) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
   187  
   188  func (g gitInfos) ContribCountPerAuthor() contribCounts {
   189  	var c contribCounts
   190  
   191  	counters := make(map[string]countribCount)
   192  
   193  	for _, gi := range g {
   194  		authorID := gi.AuthorID()
   195  		if count, ok := counters[authorID]; ok {
   196  			count.Count = count.Count + 1
   197  			counters[authorID] = count
   198  		} else {
   199  			var ghA gitHubAuthor
   200  			if gi.GitHubCommit != nil {
   201  				ghA = gi.GitHubCommit.Author
   202  			}
   203  			authorCount := countribCount{Count: 1, Author: gi.Author, GitHubAuthor: ghA}
   204  			counters[authorID] = authorCount
   205  		}
   206  	}
   207  
   208  	for _, v := range counters {
   209  		c = append(c, v)
   210  	}
   211  
   212  	sort.Sort(c)
   213  	return c
   214  }
   215  
   216  func getGitInfosBefore(ref, tag, repo, repoPath string, remote bool) (gitInfos, error) {
   217  	client := newGitHubAPI(repo)
   218  	var g gitInfos
   219  
   220  	log, err := gitLogBefore(ref, tag, repoPath)
   221  	if err != nil {
   222  		return g, err
   223  	}
   224  
   225  	log = strings.Trim(log, "\n\x1e'")
   226  	entries := strings.Split(log, "\x1e")
   227  
   228  	for _, entry := range entries {
   229  		items := strings.Split(entry, "\x1f")
   230  		gi := gitInfo{}
   231  
   232  		if len(items) > 0 {
   233  			gi.Hash = items[0]
   234  		}
   235  		if len(items) > 1 {
   236  			gi.Author = items[1]
   237  		}
   238  		if len(items) > 2 {
   239  			gi.Subject = items[2]
   240  		}
   241  		if len(items) > 3 {
   242  			gi.Body = items[3]
   243  		}
   244  
   245  		if remote && gi.Hash != "" {
   246  			gc, err := client.fetchCommit(gi.Hash)
   247  			if err == nil {
   248  				gi.GitHubCommit = &gc
   249  			}
   250  		}
   251  		g = append(g, gi)
   252  	}
   253  
   254  	return g, nil
   255  }
   256  
   257  // Ignore autogenerated commits etc. in change log. This is a regexp.
   258  const ignoredCommits = "releaser?:|snapcraft:|Merge commit|Squashed"
   259  
   260  func gitLogBefore(ref, tag, repoPath string) (string, error) {
   261  	var prevTag string
   262  	var err error
   263  	if tag != "" {
   264  		prevTag = tag
   265  	} else {
   266  		prevTag, err = gitVersionTagBefore(ref)
   267  		if err != nil {
   268  			return "", err
   269  		}
   270  	}
   271  
   272  	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}
   273  
   274  	var args []string
   275  
   276  	if repoPath != "" {
   277  		args = append([]string{"-C", repoPath}, defaultArgs...)
   278  	} else {
   279  		args = defaultArgs
   280  	}
   281  
   282  	log, err := git(args...)
   283  	if err != nil {
   284  		return ",", err
   285  	}
   286  
   287  	return log, err
   288  }
   289  
   290  func gitVersionTagBefore(ref string) (string, error) {
   291  	return gitShort("describe", "--tags", "--abbrev=0", "--always", "--match", "v[0-9]*", ref+"^")
   292  }
   293  
   294  func gitShort(args ...string) (output string, err error) {
   295  	output, err = git(args...)
   296  	return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err
   297  }
   298  
   299  func tagExists(tag string) (bool, error) {
   300  	out, err := git("tag", "-l", tag)
   301  	if err != nil {
   302  		return false, err
   303  	}
   304  
   305  	if strings.Contains(out, tag) {
   306  		return true, nil
   307  	}
   308  
   309  	return false, nil
   310  }