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 }