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  }