go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luci_notify/notify/commits.go (about) 1 // Copyright 2018 The LUCI Authors. 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 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package notify 16 17 import ( 18 "context" 19 "sort" 20 "strings" 21 "sync" 22 23 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 24 "go.chromium.org/luci/buildbucket/protoutil" 25 "go.chromium.org/luci/common/api/gitiles" 26 "go.chromium.org/luci/common/data/stringset" 27 gitpb "go.chromium.org/luci/common/proto/git" 28 "go.chromium.org/luci/common/sync/parallel" 29 30 notifypb "go.chromium.org/luci/luci_notify/api/config" 31 ) 32 33 // commitIndex finds the index of the given revision inside the list of 34 // Git commits, or -1 if the revision is not found. 35 func commitIndex(commits []*gitpb.Commit, revision string) int { 36 for i, commit := range commits { 37 if commit.Id == revision { 38 return i 39 } 40 } 41 return -1 42 } 43 44 // commitsBlamelist computes EmailNotify consisting of the blamelist (commit author 45 // emails) for a list of commits. 46 func commitsBlamelist(commits []*gitpb.Commit, template string) []EmailNotify { 47 blamelist := stringset.New(len(commits)) 48 for _, commit := range commits { 49 blamelist.Add(commit.Author.Email) 50 } 51 recipients := make([]EmailNotify, 0, len(blamelist)) 52 for recipient := range blamelist { 53 recipients = append(recipients, EmailNotify{ 54 Email: recipient, 55 Template: template, 56 }) 57 } 58 sortEmailNotify(recipients) 59 return recipients 60 } 61 62 // Checkout represents a Git checkout of multiple repositories. It is a 63 // mapping of repository URLs to Git revisions. 64 type Checkout map[string]string 65 66 // NewCheckout creates a new Checkout populated with the repositories and revision 67 // found in the GitilesCommits object. 68 func NewCheckout(commits *notifypb.GitilesCommits) Checkout { 69 results := make(Checkout, len(commits.GetCommits())) 70 for _, gitilesCommit := range commits.GetCommits() { 71 results[protoutil.GitilesRepoURL(gitilesCommit)] = gitilesCommit.Id 72 } 73 return results 74 } 75 76 // ToGitilesCommits converts the Checkout into a set of GitilesCommits which may 77 // be stored as part of a config.Builder. 78 func (c Checkout) ToGitilesCommits() *notifypb.GitilesCommits { 79 if len(c) == 0 { 80 return nil 81 } 82 result := ¬ifypb.GitilesCommits{ 83 Commits: make([]*buildbucketpb.GitilesCommit, 0, len(c)), 84 } 85 for repo, commit := range c { 86 host, project, _ := gitiles.ParseRepoURL(repo) 87 result.Commits = append(result.Commits, &buildbucketpb.GitilesCommit{ 88 Host: host, 89 Project: project, 90 Id: commit, 91 }) 92 } 93 // Sort commits, first by host, then by project. 94 sort.Slice(result.Commits, func(i, j int) bool { 95 first := result.Commits[i] 96 second := result.Commits[j] 97 hostResult := strings.Compare(first.Host, second.Host) 98 if hostResult == 0 { 99 return strings.Compare(first.Project, second.Project) < 0 100 } 101 return hostResult < 0 102 }) 103 return result 104 } 105 106 // Filter filters out repositories from the Checkout which are not in the allowlist 107 // and returns a new Checkout. 108 func (c Checkout) Filter(allowset stringset.Set) Checkout { 109 newCheckout := make(Checkout) 110 for repo, commit := range c { 111 if allowset.Has(repo) { 112 newCheckout[repo] = commit 113 } 114 } 115 return newCheckout 116 } 117 118 // Logs represents a set of Git diffs between two Checkouts. 119 // 120 // It is a mapping of repository URLs to a list of Git commits, representing 121 // the Git log for that repository. 122 type Logs map[string][]*gitpb.Commit 123 124 // ComputeLogs produces a set of Git diffs between oldCheckout and newCheckout, 125 // using the repositories in the newCheckout. historyFunc is used to grab 126 // the Git history. 127 func ComputeLogs(c context.Context, luciProject string, oldCheckout, newCheckout Checkout, history HistoryFunc) (Logs, error) { 128 var resultMu sync.Mutex 129 result := make(Logs) 130 err := parallel.WorkPool(8, func(ch chan<- func() error) { 131 for repo, revision := range newCheckout { 132 repo := repo 133 newRev := revision 134 oldRev, ok := oldCheckout[repo] 135 if !ok { 136 continue 137 } 138 host, project, _ := gitiles.ParseRepoURL(repo) 139 ch <- func() error { 140 log, err := history(c, luciProject, host, project, oldRev, newRev) 141 if err != nil { 142 return err 143 } 144 if len(log) <= 1 { 145 return nil 146 } 147 resultMu.Lock() 148 result[repo] = log[:len(log)-1] 149 resultMu.Unlock() 150 return nil 151 } 152 } 153 }) 154 return result, err 155 } 156 157 // Filter filters out repositories from the Logs which are not in the allowlist 158 // and returns a new Logs. 159 func (l Logs) Filter(allowset stringset.Set) Logs { 160 newLogs := make(Logs) 161 for repo, commits := range l { 162 if allowset.Has(repo) { 163 newLogs[repo] = commits 164 } 165 } 166 return newLogs 167 } 168 169 // Blamelist computes a set of email notifications from the Logs. 170 func (l Logs) Blamelist(template string) []EmailNotify { 171 blamelist := stringset.New(0) 172 for _, log := range l { 173 for _, commit := range log { 174 blamelist.Add(commit.Author.Email) 175 } 176 } 177 recipients := make([]EmailNotify, 0, len(blamelist)) 178 for recipient := range blamelist { 179 recipients = append(recipients, EmailNotify{ 180 Email: recipient, 181 Template: template, 182 }) 183 } 184 sortEmailNotify(recipients) 185 return recipients 186 }