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  }