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 }