github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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  type changeLog struct {
    29  	Version string
    30  	Notes   gitInfos
    31  	All     gitInfos
    32  	Docs    gitInfos
    33  
    34  	// Overall stats
    35  	Repo             *gitHubRepo
    36  	ContributorCount int
    37  	ThemeCount       int
    38  }
    39  
    40  func newChangeLog(infos, docInfos gitInfos) *changeLog {
    41  	log := &changeLog{
    42  		Docs: docInfos,
    43  	}
    44  
    45  	for _, info := range infos {
    46  		// TODO(bep) improve
    47  		if regexp.MustCompile("(?i)deprecate|note").MatchString(info.Subject) {
    48  			log.Notes = append(log.Notes, info)
    49  		}
    50  
    51  		log.All = append(log.All, info)
    52  		info.Subject = strings.TrimSpace(info.Subject)
    53  
    54  	}
    55  
    56  	return log
    57  }
    58  
    59  type gitInfo struct {
    60  	Hash    string
    61  	Author  string
    62  	Subject string
    63  	Body    string
    64  
    65  	GitHubCommit *gitHubCommit
    66  }
    67  
    68  func (g gitInfo) Issues() []int {
    69  	return extractIssues(g.Body)
    70  }
    71  
    72  func (g gitInfo) AuthorID() string {
    73  	if g.GitHubCommit != nil {
    74  		return g.GitHubCommit.Author.Login
    75  	}
    76  	return g.Author
    77  }
    78  
    79  func extractIssues(body string) []int {
    80  	var i []int
    81  	m := issueRe.FindAllStringSubmatch(body, -1)
    82  	for _, mm := range m {
    83  		issueID, err := strconv.Atoi(mm[1])
    84  		if err != nil {
    85  			continue
    86  		}
    87  		i = append(i, issueID)
    88  	}
    89  	return i
    90  }
    91  
    92  type gitInfos []gitInfo
    93  
    94  func git(args ...string) (string, error) {
    95  	cmd, _ := hexec.SafeCommand("git", args...)
    96  	out, err := cmd.CombinedOutput()
    97  	if err != nil {
    98  		return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args)
    99  	}
   100  	return string(out), nil
   101  }
   102  
   103  func getGitInfos(tag, repo, repoPath string, remote bool) (gitInfos, error) {
   104  	return getGitInfosBefore("HEAD", tag, repo, repoPath, remote)
   105  }
   106  
   107  type countribCount struct {
   108  	Author       string
   109  	GitHubAuthor gitHubAuthor
   110  	Count        int
   111  }
   112  
   113  func (c countribCount) AuthorLink() string {
   114  	if c.GitHubAuthor.HTMLURL != "" {
   115  		return fmt.Sprintf("[@%s](%s)", c.GitHubAuthor.Login, c.GitHubAuthor.HTMLURL)
   116  	}
   117  
   118  	if !strings.Contains(c.Author, "@") {
   119  		return c.Author
   120  	}
   121  
   122  	return c.Author[:strings.Index(c.Author, "@")]
   123  }
   124  
   125  type contribCounts []countribCount
   126  
   127  func (c contribCounts) Less(i, j int) bool { return c[i].Count > c[j].Count }
   128  func (c contribCounts) Len() int           { return len(c) }
   129  func (c contribCounts) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
   130  
   131  func (g gitInfos) ContribCountPerAuthor() contribCounts {
   132  	var c contribCounts
   133  
   134  	counters := make(map[string]countribCount)
   135  
   136  	for _, gi := range g {
   137  		authorID := gi.AuthorID()
   138  		if count, ok := counters[authorID]; ok {
   139  			count.Count = count.Count + 1
   140  			counters[authorID] = count
   141  		} else {
   142  			var ghA gitHubAuthor
   143  			if gi.GitHubCommit != nil {
   144  				ghA = gi.GitHubCommit.Author
   145  			}
   146  			authorCount := countribCount{Count: 1, Author: gi.Author, GitHubAuthor: ghA}
   147  			counters[authorID] = authorCount
   148  		}
   149  	}
   150  
   151  	for _, v := range counters {
   152  		c = append(c, v)
   153  	}
   154  
   155  	sort.Sort(c)
   156  	return c
   157  }
   158  
   159  func getGitInfosBefore(ref, tag, repo, repoPath string, remote bool) (gitInfos, error) {
   160  	client := newGitHubAPI(repo)
   161  	var g gitInfos
   162  
   163  	log, err := gitLogBefore(ref, tag, repoPath)
   164  	if err != nil {
   165  		return g, err
   166  	}
   167  
   168  	log = strings.Trim(log, "\n\x1e'")
   169  	entries := strings.Split(log, "\x1e")
   170  
   171  	for _, entry := range entries {
   172  		items := strings.Split(entry, "\x1f")
   173  		gi := gitInfo{}
   174  
   175  		if len(items) > 0 {
   176  			gi.Hash = items[0]
   177  		}
   178  		if len(items) > 1 {
   179  			gi.Author = items[1]
   180  		}
   181  		if len(items) > 2 {
   182  			gi.Subject = items[2]
   183  		}
   184  		if len(items) > 3 {
   185  			gi.Body = items[3]
   186  		}
   187  
   188  		if remote && gi.Hash != "" {
   189  			gc, err := client.fetchCommit(gi.Hash)
   190  			if err == nil {
   191  				gi.GitHubCommit = &gc
   192  			}
   193  		}
   194  		g = append(g, gi)
   195  	}
   196  
   197  	return g, nil
   198  }
   199  
   200  // Ignore autogenerated commits etc. in change log. This is a regexp.
   201  const ignoredCommits = "snapcraft:|Merge commit|Squashed"
   202  
   203  func gitLogBefore(ref, tag, repoPath string) (string, error) {
   204  	var prevTag string
   205  	var err error
   206  	if tag != "" {
   207  		prevTag = tag
   208  	} else {
   209  		prevTag, err = gitVersionTagBefore(ref)
   210  		if err != nil {
   211  			return "", err
   212  		}
   213  	}
   214  
   215  	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}
   216  
   217  	var args []string
   218  
   219  	if repoPath != "" {
   220  		args = append([]string{"-C", repoPath}, defaultArgs...)
   221  	} else {
   222  		args = defaultArgs
   223  	}
   224  
   225  	log, err := git(args...)
   226  	if err != nil {
   227  		return ",", err
   228  	}
   229  
   230  	return log, err
   231  }
   232  
   233  func gitVersionTagBefore(ref string) (string, error) {
   234  	return gitShort("describe", "--tags", "--abbrev=0", "--always", "--match", "v[0-9]*", ref+"^")
   235  }
   236  
   237  func gitShort(args ...string) (output string, err error) {
   238  	output, err = git(args...)
   239  	return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err
   240  }
   241  
   242  func tagExists(tag string) (bool, error) {
   243  	out, err := git("tag", "-l", tag)
   244  	if err != nil {
   245  		return false, err
   246  	}
   247  
   248  	if strings.Contains(out, tag) {
   249  		return true, nil
   250  	}
   251  
   252  	return false, nil
   253  }