github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/gerrit/client/client.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package client implements a client that can handle multiple gerrit instances
    18  // derived from https://github.com/andygrunwald/go-gerrit
    19  package client
    20  
    21  import (
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"os"
    28  	"sort"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  
    33  	gerrit "github.com/andygrunwald/go-gerrit"
    34  	"github.com/prometheus/client_golang/prometheus"
    35  	"github.com/sirupsen/logrus"
    36  
    37  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    38  	"k8s.io/apimachinery/pkg/util/sets"
    39  	"sigs.k8s.io/prow/pkg/config"
    40  	"sigs.k8s.io/prow/pkg/throttle"
    41  	"sigs.k8s.io/prow/pkg/version"
    42  )
    43  
    44  const (
    45  	// CodeReview is the default (soon to be removed) gerrit code review label
    46  	CodeReview = "Code-Review"
    47  
    48  	// Merged status indicates a Gerrit change has been merged
    49  	Merged = "MERGED"
    50  	// New status indicates a Gerrit change is new (ie pending)
    51  	New = "NEW"
    52  
    53  	// ReadyForReviewMessage are the messages for a Gerrit change if it's changed
    54  	// from Draft to Active.
    55  	// This message will be sent if users press the `MARK AS ACTIVE` button.
    56  	ReadyForReviewMessageFixed = "Set Ready For Review"
    57  	// This message will be sent if users press the `SEND AND START REVIEW` button.
    58  	ReadyForReviewMessageCustomizable = "This change is ready for review."
    59  
    60  	ResultError   = "ERROR"
    61  	ResultSuccess = "SUCCESS"
    62  )
    63  
    64  var clientMetrics = struct {
    65  	queryResults *prometheus.CounterVec
    66  }{
    67  	queryResults: prometheus.NewCounterVec(prometheus.CounterOpts{
    68  		Name: "gerrit_query_results",
    69  		Help: "Count of Gerrit API queries by instance, repo, and result.",
    70  	}, []string{
    71  		"org",
    72  		"repo",
    73  		"result",
    74  	}),
    75  }
    76  
    77  func init() {
    78  	prometheus.MustRegister(clientMetrics.queryResults)
    79  }
    80  
    81  type gerritAuthentication interface {
    82  	SetCookieAuth(name, value string)
    83  }
    84  
    85  type gerritAccount interface {
    86  	GetAccount(name string) (*gerrit.AccountInfo, *gerrit.Response, error)
    87  	SetUsername(accountID string, input *gerrit.UsernameInput) (*string, *gerrit.Response, error)
    88  }
    89  
    90  type gerritChange interface {
    91  	QueryChanges(opt *gerrit.QueryChangeOptions) (*[]gerrit.ChangeInfo, *gerrit.Response, error)
    92  	SetReview(changeID, revisionID string, input *gerrit.ReviewInput) (*gerrit.ReviewResult, *gerrit.Response, error)
    93  	ListChangeComments(changeID string) (*map[string][]gerrit.CommentInfo, *gerrit.Response, error)
    94  	GetChange(changeId string, opt *gerrit.ChangeOptions) (*ChangeInfo, *gerrit.Response, error)
    95  	SubmitChange(id string, opt *gerrit.SubmitInput) (*ChangeInfo, *gerrit.Response, error)
    96  	GetRelatedChanges(changeID string, revisionID string) (*gerrit.RelatedChangesInfo, *gerrit.Response, error)
    97  }
    98  
    99  type gerritProjects interface {
   100  	GetBranch(projectName, branchID string) (*gerrit.BranchInfo, *gerrit.Response, error)
   101  }
   102  
   103  type gerritRevision interface {
   104  	GetMergeable(changeID, revisionID string, opt *gerrit.MergableOptions) (*gerrit.MergeableInfo, *gerrit.Response, error)
   105  }
   106  
   107  // gerritInstanceHandler holds all actual gerrit handlers
   108  type gerritInstanceHandler struct {
   109  	instance string
   110  	projects map[string]*config.GerritQueryFilter
   111  
   112  	authService     gerritAuthentication
   113  	accountService  gerritAccount
   114  	changeService   gerritChange
   115  	projectService  gerritProjects
   116  	revisionService gerritRevision
   117  
   118  	log logrus.FieldLogger
   119  }
   120  
   121  // Client holds a instance:handler map
   122  type Client struct {
   123  	handlers map[string]*gerritInstanceHandler
   124  	// map of instance to gerrit account
   125  	accounts map[string]*gerrit.AccountInfo
   126  
   127  	httpClient http.Client
   128  
   129  	authentication func() (string, error)
   130  	previousToken  string
   131  	lock           sync.RWMutex
   132  }
   133  
   134  // ChangeInfo is a gerrit.ChangeInfo
   135  type ChangeInfo = gerrit.ChangeInfo
   136  
   137  // RevisionInfo is a gerrit.RevisionInfo
   138  type RevisionInfo = gerrit.RevisionInfo
   139  
   140  // FileInfo is a gerrit.FileInfo
   141  type FileInfo = gerrit.FileInfo
   142  
   143  // Map from instance name to repos to lastsync time for that repo
   144  type LastSyncState map[string]map[string]time.Time
   145  
   146  func (l LastSyncState) DeepCopy() LastSyncState {
   147  	result := LastSyncState{}
   148  	for host, lastSyncs := range l {
   149  		result[host] = map[string]time.Time{}
   150  		for projects, lastSync := range lastSyncs {
   151  			result[host][projects] = lastSync
   152  		}
   153  	}
   154  	return result
   155  }
   156  
   157  type roundTripperWithThrottleAndHeader struct {
   158  	upstream http.RoundTripper
   159  	throttle.Throttler
   160  }
   161  
   162  func (rt *roundTripperWithThrottleAndHeader) RoundTrip(r *http.Request) (*http.Response, error) {
   163  	r.Header.Add("user-agent", "prow")
   164  	// Also include component name
   165  	r.Header.Add("user-agent", "prow/"+version.Name)
   166  	// Gerrit quotas are shared across all orgs so we can omit the org field to use the global thottler.
   167  	rt.Wait(r.Context(), "")
   168  	return rt.upstream.RoundTrip(r)
   169  }
   170  
   171  // NewClient returns a new gerrit client
   172  func NewClient(instances map[string]map[string]*config.GerritQueryFilter, maxQPS, maxBurst int) (*Client, error) {
   173  	roundTripper := &roundTripperWithThrottleAndHeader{upstream: http.DefaultTransport}
   174  	roundTripper.Throttle(maxQPS*3600, maxBurst)
   175  
   176  	c := &Client{
   177  		handlers: map[string]*gerritInstanceHandler{},
   178  		accounts: map[string]*gerrit.AccountInfo{},
   179  
   180  		httpClient: http.Client{
   181  			Transport: roundTripper,
   182  		},
   183  	}
   184  
   185  	for instance := range instances {
   186  		handler, err := c.newInstanceHandler(instance, instances[instance])
   187  		if err != nil {
   188  			return nil, err
   189  		}
   190  
   191  		c.handlers[instance] = handler
   192  	}
   193  
   194  	return c, nil
   195  }
   196  
   197  func (c *Client) ApplyGlobalConfig(orgRepoConfigGetter func() *config.GerritOrgRepoConfigs, lastSyncTracker *SyncTime, cookiefilePath, tokenPathOverride string, additionalFunc func()) {
   198  	c.applyGlobalConfigOnce(orgRepoConfigGetter, lastSyncTracker, cookiefilePath, tokenPathOverride, additionalFunc)
   199  
   200  	go func() {
   201  		for {
   202  			c.applyGlobalConfigOnce(orgRepoConfigGetter, lastSyncTracker, cookiefilePath, tokenPathOverride, additionalFunc)
   203  			// No need to spin constantly, give it a break. It's ok that config change has one second delay.
   204  			time.Sleep(time.Second)
   205  		}
   206  	}()
   207  }
   208  
   209  func (c *Client) applyGlobalConfigOnce(orgRepoConfigGetter func() *config.GerritOrgRepoConfigs, lastSyncTracker *SyncTime, cookiefilePath, tokenPathOverride string, additionalFunc func()) {
   210  	orgReposConfig := orgRepoConfigGetter()
   211  	if orgReposConfig == nil {
   212  		return
   213  	}
   214  	// Use globally defined gerrit repos if present
   215  	if err := c.UpdateClients(orgReposConfig.AllRepos()); err != nil {
   216  		logrus.WithError(err).Error("Updating clients.")
   217  	}
   218  	if lastSyncTracker != nil {
   219  		if err := lastSyncTracker.update(orgReposConfig.AllRepos()); err != nil {
   220  			logrus.WithError(err).Error("Syncing states.")
   221  		}
   222  	}
   223  
   224  	if additionalFunc != nil {
   225  		additionalFunc()
   226  	}
   227  
   228  	// Authenticate creates a goroutine for rotating token secrets when called the first
   229  	// time, afterwards it only authenticate once.
   230  	c.Authenticate(cookiefilePath, tokenPathOverride)
   231  }
   232  
   233  func (c *Client) authenticateOnce() {
   234  	c.lock.RLock()
   235  	auth := c.authentication
   236  	c.lock.RUnlock()
   237  
   238  	current, err := auth()
   239  	if err != nil {
   240  		logrus.WithError(err).Error("Failed to read gerrit auth token")
   241  	}
   242  
   243  	if current == c.previousToken {
   244  		return
   245  	}
   246  
   247  	logrus.Info("New gerrit token, updating handler authentication...")
   248  	c.lock.Lock()
   249  	c.previousToken = current // We need the write lock for this.
   250  	c.lock.Unlock()
   251  
   252  	// update auth token for each instance
   253  	for _, handler := range c.getAllHandlers() {
   254  		handler.authService.SetCookieAuth("o", current)
   255  	}
   256  }
   257  
   258  // getAllHandlers copies the handler map while holding the read lock.
   259  func (c *Client) getAllHandlers() map[string]*gerritInstanceHandler {
   260  	c.lock.RLock()
   261  	copied := make(map[string]*gerritInstanceHandler, len(c.handlers))
   262  	for instance, handler := range c.handlers {
   263  		copied[instance] = handler
   264  	}
   265  	c.lock.RUnlock()
   266  	return copied
   267  }
   268  
   269  // Authenticate client calls using the specified file.
   270  // Periodically re-reads the file to check for an updated value.
   271  // cookiefilePath takes precedence over tokenPath if both are set.
   272  func (c *Client) Authenticate(cookiefilePath, tokenPath string) {
   273  	var was, auth func() (string, error)
   274  	switch {
   275  	case cookiefilePath != "":
   276  		if tokenPath != "" {
   277  			logrus.WithFields(logrus.Fields{
   278  				"cookiefile": cookiefilePath,
   279  				"token":      tokenPath,
   280  			}).Warn("Ignoring token path in favor of cookiefile")
   281  		}
   282  		auth = func() (string, error) {
   283  			// TODO(fejta): listen for changes
   284  			raw, err := os.ReadFile(cookiefilePath)
   285  			if err != nil {
   286  				return "", fmt.Errorf("read cookie: %w", err)
   287  			}
   288  			fields := strings.Fields(string(raw))
   289  			token := fields[len(fields)-1]
   290  			return token, nil
   291  		}
   292  	case tokenPath != "":
   293  		auth = func() (string, error) {
   294  			raw, err := os.ReadFile(tokenPath)
   295  			if err != nil {
   296  				return "", fmt.Errorf("read token: %w", err)
   297  			}
   298  			return strings.TrimSpace(string(raw)), nil
   299  		}
   300  	default:
   301  		logrus.Info("Using anonymous authentication to gerrit")
   302  		return
   303  	}
   304  	c.lock.Lock()
   305  	was, c.authentication = c.authentication, auth
   306  	c.lock.Unlock()
   307  	c.authenticateOnce() // Ensure requests immediately authenticated
   308  	if was == nil {
   309  		go func() {
   310  			for {
   311  				c.authenticateOnce()
   312  				time.Sleep(time.Minute)
   313  			}
   314  		}()
   315  	}
   316  }
   317  
   318  func (c *Client) newInstanceHandler(instance string, projects map[string]*config.GerritQueryFilter) (*gerritInstanceHandler, error) {
   319  	gc, err := gerrit.NewClient(instance, &c.httpClient)
   320  	if err != nil {
   321  		return nil, fmt.Errorf("failed to create gerrit client: %w", err)
   322  	}
   323  
   324  	return &gerritInstanceHandler{
   325  		instance:       instance,
   326  		projects:       projects,
   327  		authService:    gc.Authentication,
   328  		accountService: gc.Accounts,
   329  		changeService:  gc.Changes,
   330  		projectService: gc.Projects,
   331  		log:            logrus.WithField("host", instance),
   332  	}, nil
   333  }
   334  
   335  // UpdateClients update gerrit clients with new instances map
   336  func (c *Client) UpdateClients(instances map[string]map[string]*config.GerritQueryFilter) error {
   337  	// Recording in newHandlers, so that deleted instances can be handled.
   338  	newHandlers := make(map[string]*gerritInstanceHandler)
   339  	var errs []error
   340  	c.lock.Lock()
   341  	defer c.lock.Unlock()
   342  	for instance := range instances {
   343  		if handler, ok := c.handlers[instance]; ok {
   344  			// Already initialized, no need to re-initialize handler. But still need
   345  			// to remember to update projects underneath.
   346  			handler.projects = instances[instance]
   347  			newHandlers[instance] = handler
   348  			continue
   349  		}
   350  		handler, err := c.newInstanceHandler(instance, instances[instance])
   351  		if err != nil {
   352  			logrus.WithField("host", instance).WithError(err).Error("Failed to create gerrit instance handler.")
   353  			errs = append(errs, err)
   354  			continue
   355  		}
   356  		newHandlers[instance] = handler
   357  	}
   358  	c.handlers = newHandlers
   359  
   360  	return utilerrors.NewAggregate(errs)
   361  }
   362  
   363  // QueryChanges queries for all changes from all projects after lastUpdate time
   364  // returns an instance:changes map
   365  func (c *Client) QueryChanges(lastState LastSyncState, rateLimit int) map[string][]ChangeInfo {
   366  	result := map[string][]ChangeInfo{}
   367  	for _, h := range c.getAllHandlers() {
   368  		lastStateForInstance := lastState[h.instance]
   369  		changes := h.queryAllChanges(lastStateForInstance, rateLimit)
   370  		if len(changes) == 0 {
   371  			continue
   372  		}
   373  
   374  		result[h.instance] = append(result[h.instance], changes...)
   375  	}
   376  	return result
   377  }
   378  
   379  func (c *Client) QueryChangesForInstance(instance string, lastState LastSyncState, rateLimit int) []ChangeInfo {
   380  	c.lock.RLock()
   381  	h, ok := c.handlers[instance]
   382  	c.lock.RUnlock()
   383  	if !ok {
   384  		logrus.WithField("instance", instance).WithField("laststate", lastState).Warn("Instance not registered as handlers.")
   385  		return []ChangeInfo{}
   386  	}
   387  	lastStateForInstance := lastState[instance]
   388  	return h.queryAllChanges(lastStateForInstance, rateLimit)
   389  }
   390  
   391  // QueryChangesForProject queries change for a project.
   392  //
   393  // Important: this method does not update LastSyncState as it is per instance
   394  // based. It doesn't make sense to update the state as this method has no idea
   395  // whether all other projects have been queries or not yet. So caller of this
   396  // method is responsible for making sure that LastSyncState is up-to-date, if
   397  // the lastUpdate time is used by caller.
   398  func (c *Client) QueryChangesForProject(instance, project string, lastUpdate time.Time, rateLimit int, additionalFilters ...string) ([]ChangeInfo, error) {
   399  	log := logrus.WithContext(context.Background()).WithField("instance", instance)
   400  
   401  	c.lock.RLock()
   402  	h, ok := c.handlers[instance]
   403  	c.lock.RUnlock()
   404  	if !ok {
   405  		return []ChangeInfo{}, fmt.Errorf("instance handler for %q not found, it might not have been initialized yet", instance)
   406  	}
   407  
   408  	queryFilters, ok := h.projects[project]
   409  	if !ok {
   410  		return []ChangeInfo{}, fmt.Errorf("project %q from instance %q not registered in gerrit handler, it might not have been initialized yet", project, instance)
   411  	}
   412  
   413  	changes, err := h.QueryChangesForProject(log, project, lastUpdate, rateLimit, append(queryStringsFromQueryFilter(queryFilters), additionalFilters...)...)
   414  	if err != nil {
   415  		return []ChangeInfo{}, fmt.Errorf("failed to query changes for project %q of %q instance: %v", project, instance, err)
   416  	}
   417  	return changes, nil
   418  }
   419  
   420  func (c *Client) GetChange(instance, id string, addtionalFields ...string) (*ChangeInfo, error) {
   421  	c.lock.RLock()
   422  	h, ok := c.handlers[instance]
   423  	c.lock.RUnlock()
   424  	if !ok {
   425  		return nil, fmt.Errorf("not activated gerrit instance: %s", instance)
   426  	}
   427  
   428  	info, resp, err := h.changeService.GetChange(id, &gerrit.ChangeOptions{AdditionalFields: addtionalFields})
   429  
   430  	if err != nil {
   431  		return nil, fmt.Errorf("error getting current change: %w", responseBodyError(err, resp))
   432  	}
   433  
   434  	return info, nil
   435  }
   436  
   437  func (c *Client) SubmitChange(instance, id string, wait bool) (*ChangeInfo, error) {
   438  	c.lock.RLock()
   439  	h, ok := c.handlers[instance]
   440  	c.lock.RUnlock()
   441  	if !ok {
   442  		return nil, fmt.Errorf("not activated gerrit instance: %s", instance)
   443  	}
   444  
   445  	info, resp, err := h.changeService.SubmitChange(id, &gerrit.SubmitInput{WaitForMerge: wait})
   446  
   447  	if err != nil {
   448  		return nil, fmt.Errorf("error submitting current change: %w", responseBodyError(err, resp))
   449  	}
   450  
   451  	return info, nil
   452  }
   453  
   454  func (c *Client) ChangeExist(instance, id string) (bool, error) {
   455  	c.lock.RLock()
   456  	h, ok := c.handlers[instance]
   457  	c.lock.RUnlock()
   458  	if !ok {
   459  		return false, fmt.Errorf("not activated gerrit instance: %s", instance)
   460  	}
   461  
   462  	_, resp, err := h.changeService.GetChange(id, nil)
   463  
   464  	if err != nil {
   465  		if resp.StatusCode == http.StatusNotFound {
   466  			return false, nil
   467  		}
   468  		return false, fmt.Errorf("error getting current change: %w", responseBodyError(err, resp))
   469  	}
   470  
   471  	return true, nil
   472  }
   473  
   474  // responseBodyError returns the error with the response body text appended if there is any.
   475  func responseBodyError(err error, resp *gerrit.Response) error {
   476  	if resp == nil || resp.Response == nil {
   477  		return err
   478  	}
   479  	defer resp.Body.Close()
   480  	b, _ := io.ReadAll(resp.Body) // Ignore the error since this is best effort.
   481  	return fmt.Errorf("%w, response body: %q, response headers: %v", err, string(b), resp.Header)
   482  }
   483  
   484  // SetReview writes a review comment base on the change id + revision
   485  func (c *Client) SetReview(instance, id, revision, message string, labels map[string]string) error {
   486  	c.lock.RLock()
   487  	h, ok := c.handlers[instance]
   488  	c.lock.RUnlock()
   489  	if !ok {
   490  		return fmt.Errorf("not activated gerrit instance: %s", instance)
   491  	}
   492  
   493  	_, resp, err := h.changeService.SetReview(id, revision, &gerrit.ReviewInput{Message: message, Labels: labels})
   494  
   495  	if err != nil {
   496  		return fmt.Errorf("cannot comment to gerrit: %w", responseBodyError(err, resp))
   497  	}
   498  
   499  	return nil
   500  }
   501  
   502  // GetBranchRevision returns SHA of HEAD of a branch
   503  func (c *Client) GetBranchRevision(instance, project, branch string) (string, error) {
   504  	c.lock.RLock()
   505  	h, ok := c.handlers[instance]
   506  	c.lock.RUnlock()
   507  	if !ok {
   508  		return "", fmt.Errorf("not activated gerrit instance: %s", instance)
   509  	}
   510  
   511  	res, resp, err := h.projectService.GetBranch(project, branch)
   512  
   513  	if err != nil {
   514  		return "", responseBodyError(err, resp)
   515  	}
   516  
   517  	return res.Revision, nil
   518  }
   519  
   520  // Account returns gerrit account for the given instance
   521  func (c *Client) Account(instance string) (*gerrit.AccountInfo, error) {
   522  	c.lock.RLock()
   523  	existing, ok := c.accounts[instance]
   524  	c.lock.RUnlock()
   525  	if ok {
   526  		return existing, nil
   527  	}
   528  
   529  	// Looks like we need to populate the value so get the write lock, but then check again.
   530  	c.lock.Lock()
   531  	defer c.lock.Unlock()
   532  	if existing, ok := c.accounts[instance]; ok {
   533  		// We lost the race and some other thread populated the value for us.
   534  		return existing, nil
   535  	}
   536  	// We won the race, so try to poplulate the value.
   537  	handler, ok := c.handlers[instance]
   538  	if !ok {
   539  		return nil, errors.New("no handlers found")
   540  	}
   541  
   542  	self, resp, err := handler.accountService.GetAccount("self")
   543  	if err != nil {
   544  		return nil, fmt.Errorf("GetAccount() failed with new authentication: %w", responseBodyError(err, resp))
   545  
   546  	}
   547  	c.accounts[instance] = self
   548  	return c.accounts[instance], nil
   549  }
   550  
   551  func (c *Client) GetMergeableInfo(instance, changeID, revisionID string) (*gerrit.MergeableInfo, error) {
   552  	c.lock.RLock()
   553  	h, ok := c.handlers[instance]
   554  	c.lock.RUnlock()
   555  	if !ok {
   556  		return nil, fmt.Errorf("not activated Gerrit instance: %s", instance)
   557  	}
   558  
   559  	mergeableInfo, resp, err := h.revisionService.GetMergeable(changeID, revisionID, nil)
   560  
   561  	if err != nil {
   562  		return nil, responseBodyError(err, resp)
   563  	}
   564  	return mergeableInfo, nil
   565  }
   566  
   567  // private handler implementation details
   568  
   569  func (h *gerritInstanceHandler) queryAllChanges(lastState map[string]time.Time, rateLimit int) []gerrit.ChangeInfo {
   570  	result := []gerrit.ChangeInfo{}
   571  	timeNow := time.Now()
   572  	for project, filters := range h.projects {
   573  		log := h.log.WithField("repo", project)
   574  		lastUpdate, ok := lastState[project]
   575  		if !ok {
   576  			lastUpdate = timeNow
   577  			log.WithField("now", timeNow).Warn("lastState not found, defaulting to now")
   578  		}
   579  		// Ignore the error, it is already logged and we want to continue on to other projects.
   580  		changes, _ := h.QueryChangesForProject(log, project, lastUpdate, rateLimit, queryStringsFromQueryFilter(filters)...)
   581  		result = append(result, changes...)
   582  	}
   583  
   584  	return result
   585  }
   586  
   587  func parseStamp(value gerrit.Timestamp) time.Time {
   588  	return value.Time
   589  }
   590  
   591  func (h *gerritInstanceHandler) injectPatchsetMessages(change *gerrit.ChangeInfo) error {
   592  
   593  	out, _, err := h.changeService.ListChangeComments(change.ID)
   594  
   595  	if err != nil {
   596  		return err
   597  	}
   598  	outer := *out
   599  	comments, ok := outer["/PATCHSET_LEVEL"]
   600  	if !ok {
   601  		return nil
   602  	}
   603  	var changed bool
   604  	for _, c := range comments {
   605  		change.Messages = append(change.Messages, gerrit.ChangeMessageInfo{
   606  			Author:         c.Author,
   607  			Date:           *c.Updated,
   608  			Message:        c.Message,
   609  			RevisionNumber: c.PatchSet,
   610  		})
   611  		changed = true
   612  	}
   613  	if changed {
   614  		sort.SliceStable(change.Messages, func(i, j int) bool {
   615  			return change.Messages[i].Date.Before(change.Messages[j].Date.Time)
   616  		})
   617  	}
   618  	return nil
   619  }
   620  
   621  func queryStringsFromQueryFilter(filters *config.GerritQueryFilter) []string {
   622  	if filters == nil {
   623  		return nil
   624  	}
   625  
   626  	var res []string
   627  
   628  	var branchFilter []string
   629  	for _, br := range filters.Branches {
   630  		branchFilter = append(branchFilter, fmt.Sprintf("branch:%s", br))
   631  	}
   632  	if len(branchFilter) > 0 {
   633  		res = append(res, fmt.Sprintf("(%s)", strings.Join(branchFilter, "+OR+")))
   634  	}
   635  	var excludedBranchFilter []string
   636  	for _, br := range filters.ExcludedBranches {
   637  		excludedBranchFilter = append(excludedBranchFilter, fmt.Sprintf("-branch:%s", br))
   638  	}
   639  	if len(excludedBranchFilter) > 0 {
   640  		res = append(res, fmt.Sprintf("(%s)", strings.Join(excludedBranchFilter, "+AND+")))
   641  	}
   642  
   643  	return res
   644  }
   645  
   646  func (h *gerritInstanceHandler) QueryChangesForProject(log logrus.FieldLogger, project string, lastUpdate time.Time, rateLimit int, additionalFilters ...string) ([]gerrit.ChangeInfo, error) {
   647  	changes, err := h.queryChangesForProjectWithoutMetrics(log, project, lastUpdate, rateLimit, additionalFilters...)
   648  	if err != nil {
   649  		clientMetrics.queryResults.WithLabelValues(h.instance, project, ResultError).Inc()
   650  		log.WithError(err).WithFields(logrus.Fields{
   651  			"lastUpdate": lastUpdate,
   652  			"rateLimit":  rateLimit,
   653  		}).Error("Failed to query changes")
   654  	} else {
   655  		clientMetrics.queryResults.WithLabelValues(h.instance, project, ResultSuccess).Inc()
   656  	}
   657  	return changes, err
   658  }
   659  
   660  type deduper struct {
   661  	result  []gerrit.ChangeInfo
   662  	seenPos map[int]int
   663  }
   664  
   665  // dedupeIntoResult dedupes items in a slice, but preserves their order. E.g.,
   666  // [1, 2, 3, 1] results in [2, 3, 1] (the "1" that came second (last seen) is
   667  // preserved over the original "1" that came first, but also its order is at the
   668  // end as well).
   669  func (d *deduper) dedupeIntoResult(ci gerrit.ChangeInfo) {
   670  	if pos, ok := d.seenPos[ci.Number]; ok {
   671  		for ; pos < len(d.result)-1; pos++ {
   672  			d.result[pos] = d.result[pos+1]
   673  			d.seenPos[d.result[pos].Number]--
   674  		}
   675  		d.result[pos] = ci
   676  		d.seenPos[ci.Number] = pos
   677  		return
   678  	}
   679  	d.seenPos[ci.Number] = len(d.result)
   680  	d.result = append(d.result, ci)
   681  }
   682  
   683  func (h *gerritInstanceHandler) queryChangesForProjectWithoutMetrics(log logrus.FieldLogger, project string, lastUpdate time.Time, rateLimit int, additionalFilters ...string) ([]gerrit.ChangeInfo, error) {
   684  	var opt gerrit.QueryChangeOptions
   685  	opt.Query = append(opt.Query, strings.Join(append(additionalFilters, "project:"+project), "+"))
   686  	opt.AdditionalFields = []string{"CURRENT_REVISION", "CURRENT_COMMIT", "CURRENT_FILES", "MESSAGES", "LABELS"}
   687  
   688  	log = log.WithFields(logrus.Fields{"query": opt.Query, "additional_fields": opt.AdditionalFields})
   689  	var start int
   690  
   691  	// Deduplicate changes repeated due to pagination, preserving order, and
   692  	// keeping the last seen.
   693  	deduper := &deduper{
   694  		result:  []gerrit.ChangeInfo{},
   695  		seenPos: make(map[int]int),
   696  	}
   697  
   698  	for {
   699  		opt.Limit = rateLimit
   700  		opt.Start = start
   701  
   702  		// override log just for this for loop
   703  		log := log.WithField("start", opt.Start)
   704  		// The change output is sorted by the last update time, most recently updated to oldest updated.
   705  		// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
   706  
   707  		changes, resp, err := h.changeService.QueryChanges(&opt)
   708  
   709  		if err != nil {
   710  			// should not happen? Let next sync loop catch up
   711  			return nil, responseBodyError(err, resp)
   712  		}
   713  
   714  		if changes == nil || len(*changes) == 0 {
   715  			log.Info("No more changes")
   716  			return deduper.result, nil
   717  		}
   718  
   719  		log.WithField("changes", len(*changes)).Debug("Found gerrit changes from page.")
   720  
   721  		start += len(*changes)
   722  
   723  		for _, change := range *changes {
   724  			// if we already processed this change, then we stop the current sync loop
   725  			updated := parseStamp(change.Updated)
   726  
   727  			log := log.WithFields(logrus.Fields{
   728  				"change":     change.Number,
   729  				"updated":    change.Updated,
   730  				"status":     change.Status,
   731  				"lastUpdate": lastUpdate,
   732  			})
   733  
   734  			// stop when we find a change last updated before lastUpdate
   735  			if !updated.After(lastUpdate) {
   736  				log.Debug("No more recently updated changes")
   737  				return deduper.result, nil
   738  			}
   739  
   740  			// process recently updated change
   741  			switch change.Status {
   742  			case Merged:
   743  				submitted := parseStamp(*change.Submitted)
   744  				log := log.WithField("submitted", submitted)
   745  				if !submitted.After(lastUpdate) {
   746  					log.Debug("Skipping previously merged change")
   747  					continue
   748  				}
   749  				log.Debug("Found merged change")
   750  				deduper.dedupeIntoResult(change)
   751  			case New:
   752  				// we need to make sure the change update is from a fresh commit change
   753  				rev, ok := change.Revisions[change.CurrentRevision]
   754  				if !ok {
   755  					log.WithError(err).WithField("revision", change.CurrentRevision).Error("Revision not found")
   756  					continue
   757  				}
   758  
   759  				created := parseStamp(rev.Created)
   760  				log := log.WithField("created", created)
   761  				if err := h.injectPatchsetMessages(&change); err != nil {
   762  					log.WithError(err).Error("Failed to inject patchset messages")
   763  				}
   764  				changeMessages := change.Messages
   765  				var newMessages bool
   766  
   767  				for _, message := range changeMessages {
   768  					if message.RevisionNumber == rev.Number {
   769  						messageTime := parseStamp(message.Date)
   770  						if messageTime.After(lastUpdate) {
   771  							log.WithFields(logrus.Fields{
   772  								"message":     message.Message,
   773  								"messageDate": messageTime,
   774  							}).Info("New messages")
   775  							newMessages = true
   776  							break
   777  						}
   778  					}
   779  				}
   780  
   781  				if !newMessages && !created.After(lastUpdate) {
   782  					// stale commit
   783  					log.Debug("Skipping existing change")
   784  					continue
   785  				}
   786  				if !newMessages {
   787  					log.Debug("Found updated change")
   788  				}
   789  				deduper.dedupeIntoResult(change)
   790  			default:
   791  				// change has been abandoned, do nothing
   792  				log.Debug("Ignored change")
   793  			}
   794  		}
   795  	}
   796  }
   797  
   798  // ChangedFilesProvider lists (in lexicographic order) the files changed as part of a Gerrit patchset.
   799  // It includes the original paths of renamed files.
   800  func ChangedFilesProvider(changeInfo *ChangeInfo) config.ChangedFilesProvider {
   801  	return func() ([]string, error) {
   802  		if changeInfo == nil {
   803  			return nil, fmt.Errorf("programmer error! The passed '*ChangeInfo' was nil which shouldn't ever happen")
   804  		}
   805  		changed := sets.New[string]()
   806  		revision := changeInfo.Revisions[changeInfo.CurrentRevision]
   807  		for file, info := range revision.Files {
   808  			changed.Insert(file)
   809  			// If the file is renamed (R) or copied (C) the old file path is included.
   810  			// We care about the old path in the rename case, but not the copy case.
   811  			if info.Status == "R" {
   812  				changed.Insert(info.OldPath)
   813  			}
   814  		}
   815  		return sets.List(changed), nil
   816  	}
   817  }
   818  
   819  // HasRelatedChanges determines if the specified change is part of a chain.
   820  // In other words, it determines if this change depends on any other changes or if any other changes depend on this change.
   821  func (c *Client) HasRelatedChanges(instance, id, revision string) (bool, error) {
   822  	c.lock.RLock()
   823  	h, ok := c.handlers[instance]
   824  	c.lock.RUnlock()
   825  	if !ok {
   826  		return false, fmt.Errorf("not activated gerrit instance: %s", instance)
   827  	}
   828  
   829  	info, resp, err := h.changeService.GetRelatedChanges(id, revision)
   830  
   831  	if err != nil {
   832  		return false, fmt.Errorf("error getting related changes: %w", responseBodyError(err, resp))
   833  	}
   834  
   835  	return len(info.Changes) > 0, nil
   836  }