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 }