go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/internal/gerrit/utils.go (about) 1 // Copyright 2022 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 gerrit 16 17 import ( 18 "context" 19 "fmt" 20 "regexp" 21 "strings" 22 "time" 23 24 "go.chromium.org/luci/common/clock" 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/common/logging" 27 gerritpb "go.chromium.org/luci/common/proto/gerrit" 28 "go.chromium.org/luci/gae/service/info" 29 "go.chromium.org/luci/server/auth" 30 ) 31 32 // ServiceAccountEmail is a helper function to get the email address 33 // that LUCI Bisection uses to perform Gerrit actions. 34 func ServiceAccountEmail(ctx context.Context) (string, error) { 35 emailAddress, err := getServiceAccountName(ctx) 36 if err != nil { 37 // Not critical - just log the error. 38 err = errors.Annotate(err, "error getting the service account email").Err() 39 logging.Errorf(ctx, err.Error()) 40 41 // Construct the service account email from the App ID instead. 42 constructedAddress := fmt.Sprintf("%s@appspot.gserviceaccount.com", info.AppID(ctx)) 43 logging.Debugf(ctx, "using constructed service account %s instead", 44 constructedAddress) 45 return constructedAddress, nil 46 } 47 48 return emailAddress, nil 49 } 50 51 func getServiceAccountName(ctx context.Context) (string, error) { 52 signer := auth.GetSigner(ctx) 53 if signer == nil { 54 return "", errors.New("failed to get the Signer instance representing the service") 55 } 56 57 info, err := signer.ServiceInfo(ctx) 58 if err != nil { 59 return "", errors.Annotate(err, "failed to get service info").Err() 60 } 61 62 return info.ServiceAccountName, nil 63 } 64 65 // GetHost extracts the Gerrit host from the given Gerrit review URL 66 func GetHost(ctx context.Context, rawReviewURL string) (string, error) { 67 reviewURL := strings.TrimSpace(rawReviewURL) 68 pattern := regexp.MustCompile("https://([^/]+)") 69 matches := pattern.FindStringSubmatch(reviewURL) 70 if matches == nil { 71 return "", fmt.Errorf("could not find Gerrit host from review URL = '%s'", 72 reviewURL) 73 } 74 return matches[1], nil 75 } 76 77 // HasLUCIBisectionComment returns whether LUCI Bisection has previously commented 78 // on the change 79 func HasLUCIBisectionComment(ctx context.Context, change *gerritpb.ChangeInfo) (bool, error) { 80 lbAccount, err := ServiceAccountEmail(ctx) 81 if err != nil { 82 return false, err 83 } 84 85 for _, message := range change.Messages { 86 if message.Author != nil { 87 if message.Author.Email == lbAccount { 88 return true, nil 89 } 90 } 91 } 92 93 return false, nil 94 } 95 96 // IsOwnedByLUCIBisection returns whether the change is owned by LUCI Bisection 97 func IsOwnedByLUCIBisection(ctx context.Context, change *gerritpb.ChangeInfo) (bool, error) { 98 if change.Owner == nil { 99 return false, nil 100 } 101 102 lbAccount, err := ServiceAccountEmail(ctx) 103 if err != nil { 104 return false, err 105 } 106 107 return change.Owner.Email == lbAccount, nil 108 } 109 110 // IsRecentSubmit returns whether the change was submitted recently, as defined 111 // by the maximum age duration given relative to now. 112 func IsRecentSubmit(ctx context.Context, change *gerritpb.ChangeInfo, maxAge time.Duration) bool { 113 earliest := clock.Now(ctx).Add(-maxAge) 114 submitted := change.Submitted.AsTime() 115 return submitted.Equal(earliest) || submitted.After(earliest) 116 } 117 118 // currentRevisionCommit returns the commit information for the current 119 // revision of the change 120 func currentRevisionCommit(ctx context.Context, 121 change *gerritpb.ChangeInfo) (*gerritpb.CommitInfo, error) { 122 revisionInfo, ok := change.Revisions[change.CurrentRevision] 123 if !ok { 124 return nil, fmt.Errorf("could not get revision info") 125 } 126 127 commitInfo := revisionInfo.Commit 128 if commitInfo == nil { 129 return nil, fmt.Errorf("could not get commit info") 130 } 131 132 return commitInfo, nil 133 } 134 135 // HasAutoRevertOffFlagSet returns whether the change has the flag set to true 136 // to prevent auto-revert. 137 func HasAutoRevertOffFlagSet(ctx context.Context, change *gerritpb.ChangeInfo) (bool, error) { 138 message, err := CommitMessage(ctx, change) 139 if err != nil { 140 return false, err 141 } 142 143 pattern := regexp.MustCompile(`(NOAUTOREVERT)(\s)*=(\s)*true`) 144 return pattern.MatchString(message), nil 145 } 146 147 // AuthorEmail returns the email of the author of the change's current commit. 148 func AuthorEmail(ctx context.Context, change *gerritpb.ChangeInfo) (string, error) { 149 commitInfo, err := currentRevisionCommit(ctx, change) 150 if err != nil { 151 return "", err 152 } 153 154 if commitInfo.Author == nil { 155 return "", fmt.Errorf("no author in commit info") 156 } 157 158 return commitInfo.Author.Email, nil 159 } 160 161 // CommitMessage returns the commit message of the change 162 func CommitMessage(ctx context.Context, change *gerritpb.ChangeInfo) (string, error) { 163 commitInfo, err := currentRevisionCommit(ctx, change) 164 if err != nil { 165 return "", err 166 } 167 168 return commitInfo.Message, nil 169 }