github.com/olliephillips/hugo@v0.42.2/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  	"os/exec"
    19  	"regexp"
    20  	"sort"
    21  	"strconv"
    22  	"strings"
    23  )
    24  
    25  var issueRe = regexp.MustCompile(`(?i)[Updates?|Closes?|Fix.*|See] #(\d+)`)
    26  
    27  const (
    28  	notesChanges    = "notesChanges"
    29  	templateChanges = "templateChanges"
    30  	coreChanges     = "coreChanges"
    31  	outChanges      = "outChanges"
    32  	otherChanges    = "otherChanges"
    33  )
    34  
    35  type changeLog struct {
    36  	Version      string
    37  	Enhancements map[string]gitInfos
    38  	Fixes        map[string]gitInfos
    39  	Notes        gitInfos
    40  	All          gitInfos
    41  	Docs         gitInfos
    42  
    43  	// Overall stats
    44  	Repo             *gitHubRepo
    45  	ContributorCount int
    46  	ThemeCount       int
    47  }
    48  
    49  func newChangeLog(infos, docInfos gitInfos) *changeLog {
    50  	return &changeLog{
    51  		Enhancements: make(map[string]gitInfos),
    52  		Fixes:        make(map[string]gitInfos),
    53  		All:          infos,
    54  		Docs:         docInfos,
    55  	}
    56  }
    57  
    58  func (l *changeLog) addGitInfo(isFix bool, info gitInfo, category string) {
    59  	var (
    60  		infos   gitInfos
    61  		found   bool
    62  		segment map[string]gitInfos
    63  	)
    64  
    65  	if category == notesChanges {
    66  		l.Notes = append(l.Notes, info)
    67  		return
    68  	} else if isFix {
    69  		segment = l.Fixes
    70  	} else {
    71  		segment = l.Enhancements
    72  	}
    73  
    74  	infos, found = segment[category]
    75  	if !found {
    76  		infos = gitInfos{}
    77  	}
    78  
    79  	infos = append(infos, info)
    80  	segment[category] = infos
    81  }
    82  
    83  func gitInfosToChangeLog(infos, docInfos gitInfos) *changeLog {
    84  	log := newChangeLog(infos, docInfos)
    85  	for _, info := range infos {
    86  		los := strings.ToLower(info.Subject)
    87  		isFix := strings.Contains(los, "fix")
    88  		var category = otherChanges
    89  
    90  		// TODO(bep) improve
    91  		if regexp.MustCompile("(?i)deprecate").MatchString(los) {
    92  			category = notesChanges
    93  		} else if regexp.MustCompile("(?i)tpl|tplimpl:|layout").MatchString(los) {
    94  			category = templateChanges
    95  		} else if regexp.MustCompile("(?i)hugolib:").MatchString(los) {
    96  			category = coreChanges
    97  		} else if regexp.MustCompile("(?i)out(put)?:|media:|Output|Media").MatchString(los) {
    98  			category = outChanges
    99  		}
   100  
   101  		// Trim package prefix.
   102  		colonIdx := strings.Index(info.Subject, ":")
   103  		if colonIdx != -1 && colonIdx < (len(info.Subject)/2) {
   104  			info.Subject = info.Subject[colonIdx+1:]
   105  		}
   106  
   107  		info.Subject = strings.TrimSpace(info.Subject)
   108  
   109  		log.addGitInfo(isFix, info, category)
   110  	}
   111  
   112  	return log
   113  }
   114  
   115  type gitInfo struct {
   116  	Hash    string
   117  	Author  string
   118  	Subject string
   119  	Body    string
   120  
   121  	GitHubCommit *gitHubCommit
   122  }
   123  
   124  func (g gitInfo) Issues() []int {
   125  	return extractIssues(g.Body)
   126  }
   127  
   128  func (g gitInfo) AuthorID() string {
   129  	if g.GitHubCommit != nil {
   130  		return g.GitHubCommit.Author.Login
   131  	}
   132  	return g.Author
   133  }
   134  
   135  func extractIssues(body string) []int {
   136  	var i []int
   137  	m := issueRe.FindAllStringSubmatch(body, -1)
   138  	for _, mm := range m {
   139  		issueID, err := strconv.Atoi(mm[1])
   140  		if err != nil {
   141  			continue
   142  		}
   143  		i = append(i, issueID)
   144  	}
   145  	return i
   146  }
   147  
   148  type gitInfos []gitInfo
   149  
   150  func git(args ...string) (string, error) {
   151  	cmd := exec.Command("git", args...)
   152  	out, err := cmd.CombinedOutput()
   153  	if err != nil {
   154  		return "", fmt.Errorf("git failed: %q: %q (%q)", err, out, args)
   155  	}
   156  	return string(out), nil
   157  }
   158  
   159  func getGitInfos(tag, repo, repoPath string, remote bool) (gitInfos, error) {
   160  	return getGitInfosBefore("HEAD", tag, repo, repoPath, remote)
   161  }
   162  
   163  type countribCount struct {
   164  	Author       string
   165  	GitHubAuthor gitHubAuthor
   166  	Count        int
   167  }
   168  
   169  func (c countribCount) AuthorLink() string {
   170  	if c.GitHubAuthor.HtmlURL != "" {
   171  		return fmt.Sprintf("[@%s](%s)", c.GitHubAuthor.Login, c.GitHubAuthor.HtmlURL)
   172  	}
   173  
   174  	if !strings.Contains(c.Author, "@") {
   175  		return c.Author
   176  	}
   177  
   178  	return c.Author[:strings.Index(c.Author, "@")]
   179  
   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|Revert"
   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 gitLog() (string, error) {
   295  	return gitLogBefore("HEAD", "", "")
   296  }
   297  
   298  func gitShort(args ...string) (output string, err error) {
   299  	output, err = git(args...)
   300  	return strings.Replace(strings.Split(output, "\n")[0], "'", "", -1), err
   301  }
   302  
   303  func tagExists(tag string) (bool, error) {
   304  	out, err := git("tag", "-l", tag)
   305  
   306  	if err != nil {
   307  		return false, err
   308  	}
   309  
   310  	if strings.Contains(out, tag) {
   311  		return true, nil
   312  	}
   313  
   314  	return false, nil
   315  }