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 }