go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/changelist/util.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 changelist
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"sync"
    22  	"time"
    23  
    24  	"golang.org/x/sync/errgroup"
    25  
    26  	"go.chromium.org/luci/auth/identity"
    27  	"go.chromium.org/luci/common/errors"
    28  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    29  	"go.chromium.org/luci/common/retry/transient"
    30  	"go.chromium.org/luci/gae/service/datastore"
    31  
    32  	"go.chromium.org/luci/cv/internal/common"
    33  )
    34  
    35  // PanicIfNotValid checks that Snapshot stored has required fields set.
    36  func (s *Snapshot) PanicIfNotValid() {
    37  	switch {
    38  	case s == nil:
    39  	case s.GetExternalUpdateTime() == nil:
    40  		panic("missing ExternalUpdateTime")
    41  	case s.GetLuciProject() == "":
    42  		panic("missing LuciProject")
    43  	case s.GetMinEquivalentPatchset() == 0:
    44  		panic("missing MinEquivalentPatchset")
    45  	case s.GetPatchset() == 0:
    46  		panic("missing Patchset")
    47  
    48  	case s.GetGerrit() == nil:
    49  		panic("Gerrit is required, until CV supports more code reviews")
    50  	case s.GetGerrit().GetInfo() == nil:
    51  		panic("Gerrit.Info is required, until CV supports more code reviews")
    52  	}
    53  }
    54  
    55  // LoadCLsMap loads `CL` entities which are values in the provided map.
    56  //
    57  // Updates `CL` entities *in place*, but also returns them as a slice.
    58  func LoadCLsMap(ctx context.Context, m map[common.CLID]*CL) ([]*CL, error) {
    59  	cls := make([]*CL, 0, len(m))
    60  	for _, cl := range m {
    61  		cls = append(cls, cl)
    62  	}
    63  	return loadCLs(ctx, cls)
    64  }
    65  
    66  // LoadCLsByIDs loads `CL` entities of the provided list of clids.
    67  func LoadCLsByIDs(ctx context.Context, clids common.CLIDs) ([]*CL, error) {
    68  	cls := make([]*CL, len(clids))
    69  	for i, clid := range clids {
    70  		cls[i] = &CL{ID: clid}
    71  	}
    72  	return loadCLs(ctx, cls)
    73  }
    74  
    75  // LoadCLs loads given `CL` entities.
    76  func LoadCLs(ctx context.Context, cls []*CL) error {
    77  	_, err := loadCLs(ctx, cls)
    78  	return err
    79  }
    80  
    81  func loadCLs(ctx context.Context, cls []*CL) ([]*CL, error) {
    82  	err := datastore.Get(ctx, cls)
    83  	switch merr, ok := err.(errors.MultiError); {
    84  	case err == nil:
    85  		return cls, nil
    86  	case ok:
    87  		for i, err := range merr {
    88  			if err == datastore.ErrNoSuchEntity {
    89  				return nil, errors.Reason("CL %d not found in Datastore", cls[i].ID).Err()
    90  			}
    91  		}
    92  		count, err := merr.Summary()
    93  		return nil, errors.Annotate(err, "failed to load %d out of %d CLs", count, len(cls)).Tag(transient.Tag).Err()
    94  	default:
    95  		return nil, errors.Annotate(err, "failed to load %d CLs", len(cls)).Tag(transient.Tag).Err()
    96  	}
    97  }
    98  
    99  // RemoveUnusedGerritInfo mutates given ChangeInfo to remove what CV definitely
   100  // doesn't need to reduce bytes shuffled to/from Datastore.
   101  //
   102  // Doesn't complain if anything is missing.
   103  //
   104  // NOTE: keep this function actions in sync with storage.proto doc for
   105  // Gerrit.info field.
   106  func RemoveUnusedGerritInfo(ci *gerritpb.ChangeInfo) {
   107  	const keepEmail = true
   108  	const removeEmail = false
   109  	cleanUser := func(u *gerritpb.AccountInfo, keepEmail bool) {
   110  		if u == nil {
   111  			return
   112  		}
   113  		u.SecondaryEmails = nil
   114  		u.Name = ""
   115  		u.Username = ""
   116  		if !keepEmail {
   117  			u.Email = ""
   118  		}
   119  	}
   120  
   121  	cleanRevision := func(r *gerritpb.RevisionInfo) {
   122  		if r == nil {
   123  			return
   124  		}
   125  		cleanUser(r.GetUploader(), keepEmail)
   126  		r.Description = "" // patchset title.
   127  		if c := r.GetCommit(); c != nil {
   128  			c.Message = ""
   129  			c.Author = nil
   130  		}
   131  		r.Files = nil
   132  	}
   133  
   134  	cleanMessage := func(m *gerritpb.ChangeMessageInfo) {
   135  		if m == nil {
   136  			return
   137  		}
   138  		cleanUser(m.GetAuthor(), removeEmail)
   139  		cleanUser(m.GetRealAuthor(), removeEmail)
   140  	}
   141  
   142  	cleanLabel := func(l *gerritpb.LabelInfo) {
   143  		if l == nil {
   144  			return
   145  		}
   146  		all := l.GetAll()[:0]
   147  		for _, a := range l.GetAll() {
   148  			if a.GetValue() == 0 {
   149  				continue
   150  			}
   151  			cleanUser(a.GetUser(), keepEmail)
   152  			all = append(all, a)
   153  		}
   154  		l.All = all
   155  	}
   156  
   157  	for _, r := range ci.GetRevisions() {
   158  		cleanRevision(r)
   159  	}
   160  	for _, m := range ci.GetMessages() {
   161  		cleanMessage(m)
   162  	}
   163  	for _, l := range ci.GetLabels() {
   164  		cleanLabel(l)
   165  	}
   166  	cleanUser(ci.GetOwner(), keepEmail)
   167  }
   168  
   169  // OwnerIdentity is the identity of a user owning this CL.
   170  //
   171  // Snapshot must not be nil.
   172  func (s *Snapshot) OwnerIdentity() (identity.Identity, error) {
   173  	if s == nil {
   174  		panic("Snapshot is nil")
   175  	}
   176  
   177  	g := s.GetGerrit()
   178  	if g == nil {
   179  		return "", errors.New("non-Gerrit CLs not supported")
   180  	}
   181  	owner := g.GetInfo().GetOwner()
   182  	if owner == nil {
   183  		panic("Snapshot Gerrit has no owner. Bug in gerrit/updater")
   184  	}
   185  	email := owner.GetEmail()
   186  	if email == "" {
   187  		return "", errors.Reason(
   188  			"CL %s/%d owner email of account %d is unknown",
   189  			g.GetHost(), g.GetInfo().GetNumber(),
   190  			owner.GetAccountId(),
   191  		).Err()
   192  	}
   193  	return identity.MakeIdentity("user:" + email)
   194  }
   195  
   196  // IsSubmittable returns whether the change has been approved
   197  // by the project submit rules.
   198  func (s *Snapshot) IsSubmittable() (bool, error) {
   199  	if s == nil {
   200  		panic("Snapshot is nil")
   201  	}
   202  
   203  	g := s.GetGerrit()
   204  	if g == nil {
   205  		return false, errors.New("non-Gerrit CLs not supported")
   206  	}
   207  	return g.GetInfo().GetSubmittable(), nil
   208  }
   209  
   210  // IsSubmitted returns whether the change has been submitted.
   211  func (s *Snapshot) IsSubmitted() (bool, error) {
   212  	if s == nil {
   213  		panic("Snapshot is nil")
   214  	}
   215  
   216  	g := s.GetGerrit()
   217  	if g == nil {
   218  		return false, errors.New("non-Gerrit CLs not supported")
   219  	}
   220  	return g.GetInfo().GetStatus() == gerritpb.ChangeStatus_MERGED, nil
   221  }
   222  
   223  // QueryCLIDsUpdatedBefore queries all CLIDs updated before the given timestamp.
   224  //
   225  // This is mainly used for data retention purpose. Result CLIDs are sorted.
   226  func QueryCLIDsUpdatedBefore(ctx context.Context, before time.Time) (common.CLIDs, error) {
   227  	var ret common.CLIDs
   228  	var retMu sync.Mutex
   229  	eg, ectx := errgroup.WithContext(ctx)
   230  	eg.SetLimit(10)
   231  	for shard := 0; shard < retentionKeyShards; shard++ {
   232  		shard := shard
   233  		eg.Go(func() error {
   234  			q := datastore.NewQuery("CL").
   235  				Lt("RetentionKey", fmt.Sprintf("%02d/%010d", shard, before.Unix())).
   236  				Gt("RetentionKey", fmt.Sprintf("%02d/", shard)).
   237  				KeysOnly(true)
   238  			var keys []*datastore.Key
   239  			switch err := datastore.GetAll(ectx, q, &keys); {
   240  			case err != nil:
   241  				return errors.Annotate(err, "failed to query CL keys").Tag(transient.Tag).Err()
   242  			case len(keys) > 0:
   243  				retMu.Lock()
   244  				for _, key := range keys {
   245  					ret = append(ret, common.CLID(key.IntID()))
   246  				}
   247  				retMu.Unlock()
   248  			}
   249  			return nil
   250  		})
   251  	}
   252  	if err := eg.Wait(); err != nil {
   253  		return nil, err
   254  	}
   255  	sort.Sort(ret)
   256  	return ret, nil
   257  }