git.go (5567B)
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 }