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 := &notifypb.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  }