go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/gerritfake/fake.go (about) 1 // Copyright 2020 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 gerritfake 16 17 import ( 18 "context" 19 "fmt" 20 "regexp" 21 "strconv" 22 "strings" 23 "sync" 24 "time" 25 26 "google.golang.org/grpc/codes" 27 "google.golang.org/grpc/status" 28 "google.golang.org/protobuf/proto" 29 "google.golang.org/protobuf/types/known/timestamppb" 30 31 "go.chromium.org/luci/common/clock/testclock" 32 "go.chromium.org/luci/common/data/stringset" 33 gerritpb "go.chromium.org/luci/common/proto/gerrit" 34 35 "go.chromium.org/luci/cv/internal/gerrit" 36 ) 37 38 // Fake simulates Gerrit for CV tests. 39 type Fake struct { 40 // m protects all other members below. 41 m sync.Mutex 42 43 // cs is a set of changes, indexed by (host, change number). 44 // See key() function. 45 cs map[string]*Change 46 47 // parentsOf maps a change's patchset (host, change number, patchset) 48 // to one or more Git parents; each parent is another change's patchset. 49 // 50 // parentsOf[X] can be read as "changes on which X depends non-transitively". 51 // 52 // parentsOf is essentially the DAG (directed acyclic graph) that Git stores. 53 parentsOf map[string][]string 54 // childrenOf is a reverse of parentsOf. 55 // 56 // childrenOf[X] can be read as "changes which depend on X non-transitively". 57 childrenOf map[string][]string 58 59 // requests are all incoming requests that this Fake has received. 60 requests []proto.Message 61 requestsMu sync.RWMutex 62 } 63 64 // MakeClient implemnents gerrit.Factory. 65 func (f *Fake) MakeClient(ctx context.Context, gerritHost, luciProject string) (gerrit.Client, error) { 66 if strings.ContainsRune(luciProject, '.') { 67 // Quickly catch common mistake. 68 panic(fmt.Errorf("wrong gerritHost or luciProject: %q %q", gerritHost, luciProject)) 69 } 70 return &Client{f: f, luciProject: luciProject, host: gerritHost}, nil 71 } 72 73 // MakeMirrorIterator implemnents gerrit.Factory. 74 func (f *Fake) MakeMirrorIterator(ctx context.Context) *gerrit.MirrorIterator { 75 return &gerrit.MirrorIterator{""} 76 } 77 78 // Requests returns a shallow copy of all incoming requests this fake has 79 // received. 80 func (f *Fake) Requests() []proto.Message { 81 f.requestsMu.RLock() 82 defer f.requestsMu.RUnlock() 83 cpy := make([]proto.Message, len(f.requests)) 84 copy(cpy, f.requests) 85 return cpy 86 } 87 88 func (f *Fake) recordRequest(req proto.Message) { 89 f.requestsMu.Lock() 90 defer f.requestsMu.Unlock() 91 f.requests = append(f.requests, proto.Clone(req)) 92 } 93 94 // Change = change details + ACLs. 95 type Change struct { 96 Host string 97 Info *gerritpb.ChangeInfo 98 ACLs AccessCheck 99 } 100 101 // Copy deep-copies a Change. 102 // NOTE: ACLs, which is a reference to a func, isn't deep-copied. 103 func (c *Change) Copy() *Change { 104 r := &Change{ 105 Host: c.Host, 106 Info: proto.Clone(c.Info).(*gerritpb.ChangeInfo), 107 ACLs: c.ACLs, 108 } 109 return r 110 } 111 112 type AccessCheck func(op Operation, luciProject string) *status.Status 113 114 type Operation int 115 116 const ( 117 // OpRead gates Fetch CL metadata, files, related CLs. 118 OpRead Operation = iota 119 // OpReview gates posting comments and votes on one's own behalf. 120 // 121 // NOTE: The actual Gerrit service has per-label ACLs for voting, but CV 122 // doesn't vote on its own. 123 OpReview 124 // OpAlterVotesOfOthers gates altering votes of behalf of others. 125 OpAlterVotesOfOthers 126 // OpSubmit gates submitting. 127 OpSubmit 128 ) 129 130 /////////////////////////////////////////////////////////////////////////////// 131 // Antiboilerplate functions to reduce verbosity in tests. 132 133 // WithCLs returns Fake with several changes. 134 func WithCLs(cs ...*Change) *Fake { 135 f := &Fake{ 136 cs: make(map[string]*Change, len(cs)), 137 } 138 for _, c := range cs { 139 cpy := &Change{ 140 Host: c.Host, 141 ACLs: c.ACLs, 142 Info: &gerritpb.ChangeInfo{}, 143 } 144 proto.Merge(cpy.Info, c.Info) 145 f.cs[c.key()] = cpy 146 } 147 return f 148 } 149 150 // WithCIs returns a Fake with one change per passed ChangeInfo sharing the same 151 // host and ACLs. 152 func WithCIs(host string, acls AccessCheck, cis ...*gerritpb.ChangeInfo) *Fake { 153 f := &Fake{} 154 f.cs = make(map[string]*Change, len(cis)) 155 for _, ci := range cis { 156 c := &Change{ 157 Host: host, 158 ACLs: acls, 159 Info: &gerritpb.ChangeInfo{}, 160 } 161 proto.Merge(c.Info, ci) 162 f.cs[c.key()] = c 163 } 164 return f 165 } 166 167 // AddFrom adds all changes from another fake to the this fake and returns this 168 // fake. 169 // 170 // Changes are added by reference. Primarily useful to construct Fake with CLs 171 // on several hosts, e.g.: 172 // 173 // fake := WithCIs(hostA, aclA, ciA1, ciA2).AddFrom(hostB, aclB, ciB1) 174 func (f *Fake) AddFrom(other *Fake) *Fake { 175 f.m.Lock() 176 defer f.m.Unlock() 177 other.m.Lock() 178 defer other.m.Unlock() 179 180 if f.cs == nil { 181 f.cs = make(map[string]*Change, len(other.cs)) 182 } 183 for k, c := range other.cs { 184 if f.cs[k] != nil { 185 panic(fmt.Errorf("change %s defined in both fakes", k)) 186 } 187 f.cs[k] = c 188 } 189 190 if f.childrenOf == nil { 191 f.childrenOf = make(map[string][]string, len(other.childrenOf)) 192 } 193 for k, vs := range other.childrenOf { 194 f.childrenOf[k] = append(f.childrenOf[k], vs...) 195 } 196 197 if f.parentsOf == nil { 198 f.parentsOf = make(map[string][]string, len(other.parentsOf)) 199 } 200 for k, vs := range other.parentsOf { 201 f.parentsOf[k] = append(f.parentsOf[k], vs...) 202 } 203 return f 204 } 205 206 type CIModifier func(ci *gerritpb.ChangeInfo) 207 208 // CI creates a new ChangeInfo with 1 patchset with status NEW and without any 209 // votes. 210 func CI(change int, mods ...CIModifier) *gerritpb.ChangeInfo { 211 rev := Rev(change, 1) 212 ci := &gerritpb.ChangeInfo{ 213 Number: int64(change), 214 Project: "infra/infra", 215 Ref: "refs/heads/main", 216 Status: gerritpb.ChangeStatus_NEW, 217 Owner: U("owner-99"), 218 219 Created: timestamppb.New(testclock.TestRecentTimeUTC.Add(1 * time.Hour)), 220 Updated: timestamppb.New(testclock.TestRecentTimeUTC.Add(2 * time.Hour)), 221 222 CurrentRevision: rev, 223 Revisions: map[string]*gerritpb.RevisionInfo{ 224 rev: RevInfo(1), 225 }, 226 } 227 for _, m := range mods { 228 m(ci) 229 } 230 return ci 231 } 232 233 func RevInfo(ps int) *gerritpb.RevisionInfo { 234 return &gerritpb.RevisionInfo{ 235 Number: int32(ps), 236 Kind: gerritpb.RevisionInfo_REWORK, 237 Created: timestamppb.New(testclock.TestRecentTimeUTC.Add(1 * time.Hour).Add(time.Duration(ps) * time.Minute)), 238 Files: map[string]*gerritpb.FileInfo{ 239 fmt.Sprintf("ps%03d/c.cpp", ps): {Status: gerritpb.FileInfo_W}, 240 "shared/s.py": {Status: gerritpb.FileInfo_W}, 241 }, 242 Commit: &gerritpb.CommitInfo{ 243 Id: "", // Id isn't set by Gerrit. It's set as a key in the revisions map. 244 Parents: []*gerritpb.CommitInfo_Parent{ 245 {Id: "fake_parent_commit"}, 246 }, 247 Message: "Commit.\n\nDescription.", 248 }, 249 } 250 } 251 252 // Rev generates revision in the form "rev-000006-013" where 6 and 13 are change and 253 // patchset numbers, respectively. 254 func Rev(ch, ps int) string { 255 return fmt.Sprintf("rev-%06d-%03d", ch, ps) 256 } 257 258 // RelatedChange returns ChangeAndCommit for the GetRelatedChangesResponse. 259 // 260 // Parents can be specified in several ways: 261 // - gerritpb.CommitInfo_Parent 262 // - gerritpb.CommitInfo 263 // - "<change>_<patchset>", e.g. "123_4" 264 // - "<revision>" (without underscores). 265 func RelatedChange(change, ps, curPs int, parents ...any) *gerritpb.GetRelatedChangesResponse_ChangeAndCommit { 266 prs := make([]*gerritpb.CommitInfo_Parent, len(parents)) 267 for i, pi := range parents { 268 switch v := pi.(type) { 269 case *gerritpb.CommitInfo_Parent: 270 prs[i] = v 271 case *gerritpb.CommitInfo: 272 prs[i] = &gerritpb.CommitInfo_Parent{Id: v.GetId()} 273 case string: 274 if j := strings.IndexRune(v, '_'); j != -1 { 275 prs[i] = &gerritpb.CommitInfo_Parent{Id: Rev(atoi(v[:j]), atoi(v[j+1:]))} 276 } else { 277 prs[i] = &gerritpb.CommitInfo_Parent{Id: v} 278 } 279 default: 280 panic(fmt.Errorf("unsupported type %T as commit parent #%d", pi, i)) 281 } 282 } 283 return &gerritpb.GetRelatedChangesResponse_ChangeAndCommit{ 284 CurrentPatchset: int64(curPs), 285 Number: int64(change), 286 Patchset: int64(ps), 287 Commit: &gerritpb.CommitInfo{ 288 Id: Rev(change, ps), 289 Parents: prs, 290 }, 291 } 292 } 293 294 // ACLRestricted grants full access to specified projects only. 295 func ACLRestricted(luciProjects ...string) AccessCheck { 296 ps := stringset.NewFromSlice(luciProjects...) 297 return func(_ Operation, luciProject string) *status.Status { 298 if ps.Has(luciProject) { 299 return status.New(codes.OK, "") 300 } 301 return status.New(codes.NotFound, "") 302 } 303 } 304 305 // ACLPublic grants what every registered user can do on public projects. 306 func ACLPublic() AccessCheck { 307 return func(op Operation, _ string) *status.Status { 308 switch op { 309 case OpRead, OpReview: 310 return status.New(codes.OK, "") 311 default: 312 return status.New(codes.PermissionDenied, "can read, can't modify") 313 } 314 } 315 } 316 317 // ACLReadOnly grants read-only access to the given projects. 318 func ACLReadOnly(luciProjects ...string) AccessCheck { 319 ps := stringset.NewFromSlice(luciProjects...) 320 return func(op Operation, p string) *status.Status { 321 switch { 322 case !ps.Has(p): 323 return status.New(codes.NotFound, "") 324 case op == OpRead: 325 return status.New(codes.OK, "") 326 default: 327 return status.New(codes.PermissionDenied, "can read, can't modify") 328 } 329 } 330 } 331 332 // ACLGrant grants a permission to given projects. 333 func ACLGrant(op Operation, code codes.Code, luciProjects ...string) AccessCheck { 334 ps := stringset.NewFromSlice(luciProjects...) 335 return func(o Operation, p string) *status.Status { 336 if ps.Has(p) && o == op { 337 return status.New(codes.OK, "") 338 } 339 return status.New(code, "") 340 } 341 } 342 343 // Or returns the "less restrictive" status of the 2+ AccessChecks. 344 // 345 // {OK, FAILED_PRECONDITION} <= PERMISSION_DENIED <= NOT_FOUND. 346 // Doesn't work well with other statuses. 347 func (a AccessCheck) Or(bs ...AccessCheck) AccessCheck { 348 return func(op Operation, luciProject string) *status.Status { 349 ret := a(op, luciProject) 350 switch ret.Code() { 351 case codes.OK, codes.FailedPrecondition: 352 return ret 353 } 354 for _, b := range bs { 355 s := b(op, luciProject) 356 switch s.Code() { 357 case codes.OK, codes.FailedPrecondition: 358 return s 359 case codes.PermissionDenied: 360 ret = s 361 } 362 } 363 return ret 364 } 365 } 366 367 /////////////////////////////////////////////////////////////////////////////// 368 // CI Modifiers 369 370 // PS ensures ChangeInfo's CurrentRevision corresponds to given patchset, 371 // and deletes all revisions with bigger patchsets. 372 func PS(ps int) CIModifier { 373 return func(ci *gerritpb.ChangeInfo) { 374 var toDelete []string 375 found := false 376 for rev, ri := range ci.GetRevisions() { 377 switch latest := int(ri.GetNumber()); { 378 case latest == ps: 379 ci.CurrentRevision = rev 380 found = true 381 case latest > ps: 382 toDelete = append(toDelete, rev) 383 } 384 } 385 for _, rev := range toDelete { 386 delete(ci.GetRevisions(), rev) 387 } 388 if !found { 389 rev := Rev(int(ci.GetNumber()), ps) 390 ci.CurrentRevision = rev 391 ci.GetRevisions()[rev] = RevInfo(int(ps)) 392 } 393 } 394 } 395 396 // PSWithUploader does the same as PS, but attaches a user and creation 397 // timestamp to the patchset. 398 func PSWithUploader(ps int, username string, creationTime time.Time) CIModifier { 399 barePS := PS(ps) 400 return func(ci *gerritpb.ChangeInfo) { 401 barePS(ci) 402 for _, ri := range ci.GetRevisions() { 403 if int(ri.GetNumber()) == ps { 404 ri.Uploader = U(username) 405 ri.Created = timestamppb.New(creationTime) 406 } 407 } 408 } 409 } 410 411 // AllRevs ensures ChangeInfo has a RevisionInfo per each revision 412 // corresponding to patchsets 1..current. 413 func AllRevs() CIModifier { 414 return func(ci *gerritpb.ChangeInfo) { 415 max := int(ci.GetRevisions()[ci.GetCurrentRevision()].GetNumber()) 416 found := make([]bool, max) 417 for _, ri := range ci.GetRevisions() { 418 found[ri.GetNumber()-1] = true 419 } 420 for i, f := range found { 421 if !f { 422 ps := i + 1 423 ci.GetRevisions()[Rev(int(ci.GetNumber()), ps)] = RevInfo(ps) 424 } 425 } 426 } 427 } 428 429 // Files sets ChangeInfo's current revision to contain given files. 430 func Files(fs ...string) CIModifier { 431 return func(ci *gerritpb.ChangeInfo) { 432 ri := ci.GetRevisions()[ci.GetCurrentRevision()] 433 m := make(map[string]*gerritpb.FileInfo, len(fs)) 434 for _, f := range fs { 435 // CV doesn't actually care what status is. 436 m[f] = &gerritpb.FileInfo{} 437 } 438 ri.Files = m 439 } 440 } 441 442 // Desc sets commit message, aka CL description, for ChangeInfo's current 443 // revision. 444 func Desc(cldescription string) CIModifier { 445 return func(ci *gerritpb.ChangeInfo) { 446 ri := ci.GetRevisions()[ci.GetCurrentRevision()] 447 ri.GetCommit().Message = cldescription 448 } 449 } 450 451 // Owner sets .Owner to the given username. 452 // 453 // See U() for format. 454 func Owner(username string) CIModifier { 455 a := U(username) // fail fast if wrong format 456 return func(ci *gerritpb.ChangeInfo) { 457 ci.Owner = a 458 } 459 } 460 461 // Updated sets .Updated to the given time. 462 func Updated(t time.Time) CIModifier { 463 return func(ci *gerritpb.ChangeInfo) { 464 ci.Updated = timestamppb.New(t) 465 } 466 } 467 468 // Ref sets .Ref to the given ref. 469 func Ref(ref string) CIModifier { 470 return func(ci *gerritpb.ChangeInfo) { 471 if !strings.HasPrefix(ref, "refs/") { 472 panic(fmt.Errorf("ref must start with 'refs/', but %q given", ref)) 473 } 474 ci.Ref = ref 475 } 476 } 477 478 // Project sets .Project to the given Gerrit project. 479 func Project(p string) CIModifier { 480 return func(ci *gerritpb.ChangeInfo) { 481 ci.Project = p 482 } 483 } 484 485 // Status sets .Status to the given status. 486 // Either a string or value of gerritpb.ChangeStatus. 487 func Status(s any) CIModifier { 488 return func(ci *gerritpb.ChangeInfo) { 489 switch v := s.(type) { 490 case gerritpb.ChangeStatus: 491 ci.Status = v 492 return 493 case string: 494 if i, exists := gerritpb.ChangeStatus_value[v]; exists { 495 ci.Status = gerritpb.ChangeStatus(i) 496 return 497 } 498 } 499 panic(fmt.Errorf("unrecognized status %v", s)) 500 } 501 } 502 503 // Messages sets .Messages to the given messages. 504 func Messages(msgs ...*gerritpb.ChangeMessageInfo) CIModifier { 505 return func(ci *gerritpb.ChangeInfo) { 506 ci.Messages = msgs 507 } 508 } 509 510 // Vote sets a label to the given value by the given user(s) on the latest 511 // patchset. 512 func Vote(label string, value int, timeAndUser ...any) CIModifier { 513 var who *gerritpb.AccountInfo 514 var when time.Time 515 switch { 516 case len(timeAndUser) == 0: 517 // Larger than default rev creation time even with lots of patchsets. 518 when = testclock.TestRecentTimeUTC.Add(10 * time.Hour) 519 who = U("user-1") 520 case len(timeAndUser) != 2: 521 panic(fmt.Errorf("incorrect usage, must have 2 params, not %d", len(timeAndUser))) 522 default: 523 var ok bool 524 if when, ok = timeAndUser[0].(time.Time); !ok { 525 panic(fmt.Errorf("expected time.Time, got %T", timeAndUser[0])) 526 } 527 528 switch v := timeAndUser[1].(type) { 529 case *gerritpb.AccountInfo: 530 who = v 531 case string: 532 who = U(v) 533 default: 534 panic(fmt.Errorf("expected *gerritpb.AccountInfo or string, got %T", v)) 535 } 536 } 537 538 ai := &gerritpb.ApprovalInfo{ 539 User: who, 540 Date: timestamppb.New(when), 541 Value: int32(value), 542 } 543 return func(ci *gerritpb.ChangeInfo) { 544 if ci.GetLabels() == nil { 545 ci.Labels = map[string]*gerritpb.LabelInfo{} 546 } 547 switch li, ok := ci.GetLabels()[label]; { 548 case !ok: 549 ci.GetLabels()[label] = &gerritpb.LabelInfo{ 550 All: []*gerritpb.ApprovalInfo{ai}, 551 } 552 case ok: 553 for i, existing := range li.GetAll() { 554 if existing.GetUser().GetAccountId() == ai.GetUser().GetAccountId() { 555 li.All[i] = ai 556 return 557 } 558 } 559 li.All = append(li.GetAll(), ai) 560 } 561 } 562 } 563 564 // CQ is a shorthand for Vote("Commit-Queue", ...). 565 func CQ(value int, timeAndUser ...any) CIModifier { 566 return Vote("Commit-Queue", value, timeAndUser...) 567 } 568 569 // Approve sets Submittable to true. 570 func Approve() CIModifier { 571 return func(ci *gerritpb.ChangeInfo) { 572 ci.Submittable = true 573 } 574 } 575 576 // Disapprove sets Submittable to false. 577 func Disapprove() CIModifier { 578 return func(ci *gerritpb.ChangeInfo) { 579 ci.Submittable = false 580 } 581 } 582 583 // Reviewer sets the reviewers of the CL. 584 func Reviewer(rs ...*gerritpb.AccountInfo) CIModifier { 585 return func(ci *gerritpb.ChangeInfo) { 586 if ci.Reviewers == nil { 587 ci.Reviewers = &gerritpb.ReviewerStatusMap{} 588 } 589 ci.Reviewers.Reviewers = rs 590 } 591 } 592 593 var usernameToAccountIDRegexp = regexp.MustCompile(`^.+[-.\alpha](\d+)$`) 594 595 // U returns a Gerrit User for `username`@example.com as gerritpb.AccountInfo. 596 // 597 // AccountID is either 1 or taken from the ending digits of a username. 598 func U(username string) *gerritpb.AccountInfo { 599 accountID := int64(1) 600 if subs := usernameToAccountIDRegexp.FindSubmatch([]byte(username)); len(subs) > 0 { 601 i, err := strconv.ParseInt(string(subs[1]), 10, 64) 602 if err != nil { 603 panic(err) 604 } 605 accountID = i 606 } 607 email := username + "@example.com" 608 return &gerritpb.AccountInfo{ 609 Email: email, 610 AccountId: accountID, 611 } 612 } 613 614 // MetaRevID sets .MetaRevID for the given change. 615 func MetaRevID(metaRevID string) CIModifier { 616 return func(ci *gerritpb.ChangeInfo) { 617 ci.MetaRevId = metaRevID 618 } 619 } 620 621 // ParentCommits sets the parent commits for the current revision. 622 func ParentCommits(parents []string) CIModifier { 623 return func(ci *gerritpb.ChangeInfo) { 624 if ci.GetCurrentRevision() == "" { 625 panic("missing current revision") 626 } 627 revInfo, ok := ci.GetRevisions()[ci.GetCurrentRevision()] 628 if !ok { 629 panic("missing revision info for current revision") 630 } 631 632 revInfo.GetCommit().Parents = make([]*gerritpb.CommitInfo_Parent, len(parents)) 633 for i, parent := range parents { 634 revInfo.GetCommit().Parents[i] = &gerritpb.CommitInfo_Parent{ 635 Id: parent, 636 } 637 } 638 } 639 } 640 641 /////////////////////////////////////////////////////////////////////////////// 642 // Getters / Mutators 643 644 // Has returns if given change exists. 645 func (f *Fake) Has(host string, change int) bool { 646 f.m.Lock() 647 defer f.m.Unlock() 648 _, ok := f.cs[key(host, change)] 649 return ok 650 } 651 652 // GetChange returns a copy of a Change that must exist. Panics otherwise. 653 func (f *Fake) GetChange(host string, change int) *Change { 654 f.m.Lock() 655 defer f.m.Unlock() 656 c, ok := f.cs[key(host, change)] 657 if !ok { 658 panic(fmt.Errorf("CL %s/%d not found", host, change)) 659 } 660 return c.Copy() 661 } 662 663 // CreateChange adds a change that must not yet exist. 664 func (f *Fake) CreateChange(c *Change) { 665 f.m.Lock() 666 defer f.m.Unlock() 667 k := key(c.Host, int(c.Info.GetNumber())) 668 if f.cs == nil { 669 f.cs = map[string]*Change{k: c} 670 return 671 } 672 if _, ok := f.cs[k]; ok { 673 panic(fmt.Errorf("CL %s already exists", k)) 674 } 675 f.cs[k] = c.Copy() 676 } 677 678 // MutateChange modifies a change while holding a lock blocking concurrent RPCs. 679 // Change must exist. Panics otherwise. 680 func (f *Fake) MutateChange(host string, change int, mut func(c *Change)) { 681 k := key(host, change) 682 683 f.m.Lock() 684 defer f.m.Unlock() 685 c, ok := f.cs[k] 686 if !ok { 687 panic(fmt.Errorf("CL %s/%d not found", host, change)) 688 } 689 mut(c) 690 // Make a copy, to avoid accidental mutation at call sites. 691 f.cs[k] = c.Copy() 692 } 693 694 // DeleteChange deletes a change that must exist. Panics otherwise. 695 func (f *Fake) DeleteChange(host string, change int) { 696 k := key(host, change) 697 f.m.Lock() 698 defer f.m.Unlock() 699 if _, ok := f.cs[k]; !ok { 700 panic(fmt.Errorf("CL %s/%d not found", host, change)) 701 } 702 delete(f.cs, k) 703 } 704 705 // SetDependsOn establishes Git relationship between a child CL and 1 or more 706 // parents, which are considered dependencies of the child CL. 707 // 708 // Child and each parent can be specified as either: 709 // - Change or ChangeInfo, in which case their current patchset is used, 710 // - <change>_<patchset>, e.g. "10_3". 711 func (f *Fake) SetDependsOn(host string, child any, parents ...any) { 712 f.m.Lock() 713 defer f.m.Unlock() 714 if f.parentsOf == nil { 715 f.parentsOf = make(map[string][]string, 1) 716 } 717 if f.childrenOf == nil { 718 f.childrenOf = make(map[string][]string, len(parents)) 719 } 720 721 ch, ps := parseChangePatchset(child) 722 ckey := psKey(host, ch, ps) 723 if _, _, _, err := f.resolvePSKeyLocked(ckey); err != nil { 724 panic(err) 725 } 726 for _, p := range parents { 727 ch, ps = parseChangePatchset(p) 728 pkey := psKey(host, ch, ps) 729 if pkey == ckey { 730 panic(fmt.Errorf("same child %q and parent %q", ckey, pkey)) 731 } 732 if _, _, _, err := f.resolvePSKeyLocked(pkey); err != nil { 733 panic(err) 734 } 735 f.parentsOf[ckey] = append(f.parentsOf[ckey], pkey) 736 f.childrenOf[pkey] = append(f.childrenOf[pkey], ckey) 737 } 738 } 739 740 /////////////////////////////////////////////////////////////////////////////// 741 // Helpers 742 743 func (c *Change) key() string { 744 return key(c.Host, int(c.Info.GetNumber())) 745 } 746 747 func key(host string, change int) string { 748 return fmt.Sprintf("%s/%d", host, change) 749 } 750 751 func psKey(host string, change, ps int) string { 752 return fmt.Sprintf("%s/%d/%d", host, change, ps) 753 } 754 755 func splitPSKey(k string) (key string, ps int) { 756 i := strings.LastIndex(k, "/") 757 return k[:i], atoi(k[i+1:]) 758 } 759 760 func (c *Change) resolveRevision(r string) (int, *gerritpb.RevisionInfo, error) { 761 if ri, ok := c.Info.GetRevisions()[r]; ok { 762 return int(ri.GetNumber()), ri, nil 763 } 764 if ps, err := strconv.Atoi(r); err == nil { 765 _, ri := c.findRevisionForPS(ps) 766 if ri != nil { 767 return ps, ri, nil 768 } 769 } 770 return 0, nil, status.Errorf(codes.NotFound, 771 "couldn't resolve change %d revision %q", c.Info.GetNumber(), r) 772 } 773 774 func (c *Change) findRevisionForPS(ps int) (rev string, ri *gerritpb.RevisionInfo) { 775 for rev, ri := range c.Info.GetRevisions() { 776 if ri.GetNumber() == int32(ps) { 777 return rev, ri 778 } 779 } 780 return "", nil 781 } 782 783 func atoi64(s string) int64 { 784 a, err := strconv.ParseInt(s, 10, 64) 785 if err != nil { 786 panic(fmt.Errorf("invalid int %q: %s", s, err)) 787 } 788 return a 789 } 790 791 func atoi(s string) int { 792 return int(atoi64(s)) 793 } 794 795 func parseChangePatchset(s any) (int, int) { 796 switch v := s.(type) { 797 case *gerritpb.ChangeInfo: 798 return int(v.GetNumber()), int(v.GetRevisions()[v.GetCurrentRevision()].GetNumber()) 799 case *Change: 800 return parseChangePatchset(v.Info) 801 case string: 802 if j := strings.IndexRune(v, '_'); j != -1 { 803 return int(atoi64(v[:j])), int(atoi64(v[j+1:])) 804 } 805 panic(fmt.Errorf("unsupported %q: use change_patchset e.g. 123_1", v)) 806 default: 807 panic(fmt.Errorf("unsupported type %T %v as change patchset", s, v)) 808 } 809 } 810 811 func (f *Fake) resolvePSKeyLocked(psk string) (ch *Change, rev string, ri *gerritpb.RevisionInfo, err error) { 812 k, ps := splitPSKey(psk) 813 var ok bool 814 ch, ok = f.cs[k] 815 if !ok { 816 err = status.Errorf(codes.Unknown, "fake relation chain invalid: missing %s change", k) 817 return 818 } 819 rev, ri = ch.findRevisionForPS(ps) 820 if ri == nil { 821 err = status.Errorf(codes.Unknown, "fake relation chain invalid: missing patchset %d for %s change", ps, k) 822 } 823 return 824 }