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 }