go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/bisection/culpritaction/revertculprit/createrevert.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 revertculprit
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"regexp"
    21  	"strings"
    22  
    23  	"go.chromium.org/luci/bisection/internal/config"
    24  	"go.chromium.org/luci/bisection/internal/gerrit"
    25  	"go.chromium.org/luci/bisection/model"
    26  	bisectionpb "go.chromium.org/luci/bisection/proto/v1"
    27  	"go.chromium.org/luci/bisection/util/datastoreutil"
    28  
    29  	"go.chromium.org/luci/common/errors"
    30  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    31  )
    32  
    33  // isCulpritRevertible returns:
    34  //   - whether a revert should be created for the culprit CL;
    35  //   - the reason it should not be created if applicable; and
    36  //   - the error if one occurred.
    37  func isCulpritRevertible(ctx context.Context, gerritClient *gerrit.Client,
    38  	culprit *gerritpb.ChangeInfo, culpritModel *model.Suspect, project string) (bool, string, error) {
    39  	// We only create revert if it belongs to a builder being watched
    40  	// by sheriffs.
    41  	if culpritModel.AnalysisType == bisectionpb.AnalysisType_TEST_FAILURE_ANALYSIS {
    42  		tfa, err := datastoreutil.GetTestFailureAnalysisForSuspect(ctx, culpritModel)
    43  		if err != nil {
    44  			return false, "", errors.Annotate(err, "get test failure analysis for suspect").Err()
    45  		}
    46  		if len(tfa.SheriffRotations) == 0 {
    47  			return false, "the builder of the failed test(s) is not being watched by gardeners", nil
    48  		}
    49  	} else if culpritModel.AnalysisType == bisectionpb.AnalysisType_COMPILE_FAILURE_ANALYSIS {
    50  		buildID, err := datastoreutil.GetBuildIDForCompileSuspect(ctx, culpritModel)
    51  		if err != nil {
    52  			return false, "", errors.Annotate(err, "get build id for suspect").Err()
    53  		}
    54  		build, err := datastoreutil.GetBuild(ctx, buildID)
    55  		if err != nil {
    56  			return false, "", errors.Annotate(err, "get build").Err()
    57  		}
    58  		if build == nil {
    59  			return false, "", errors.Reason("no build found: %d", buildID).Err()
    60  		}
    61  		if len(build.SheriffRotations) == 0 {
    62  			return false, "the associated builder is not being watched by gardeners", nil
    63  		}
    64  	}
    65  
    66  	// Check if the culprit's description has disabled autorevert
    67  	hasFlag, err := gerrit.HasAutoRevertOffFlagSet(ctx, culprit)
    68  	if err != nil {
    69  		return false, "", errors.Annotate(err, "error checking for auto-revert flag").Err()
    70  	}
    71  	if hasFlag {
    72  		return false, "auto-revert has been disabled for this CL by its description", nil
    73  	}
    74  
    75  	// Check if the author of the culprit is irrevertible
    76  	cannotRevert, err := HasIrrevertibleAuthor(ctx, culprit)
    77  	if err != nil {
    78  		return false, "", errors.Annotate(err, "error getting culprit's commit author").Err()
    79  	}
    80  	if cannotRevert {
    81  		return false, "LUCI Bisection cannot revert changes from this CL's author", nil
    82  	}
    83  
    84  	// Check if there are other merged changes depending on the culprit
    85  	hasDep, err := gerritClient.HasDependency(ctx, culprit)
    86  	if err != nil {
    87  		return false, "", errors.Annotate(err, "error checking for dependencies").Err()
    88  	}
    89  	if hasDep {
    90  		return false, "there are merged changes depending on it", nil
    91  	}
    92  	// Check if LUCI Bisection's Gerrit config allows revert creation
    93  	gerritCfg, err := config.GetGerritCfgForSuspect(ctx, culpritModel, project)
    94  	if err != nil {
    95  		return false, "", errors.Annotate(err, "error fetching configs").Err()
    96  	}
    97  	canCreate, reason, err := config.CanCreateRevert(ctx, gerritCfg, culpritModel.AnalysisType)
    98  	if err != nil {
    99  		return false, "", errors.Annotate(err, "error checking Create Revert configs").Err()
   100  	}
   101  	if !canCreate {
   102  		return false, reason, nil
   103  	}
   104  	return true, "", nil
   105  }
   106  
   107  // createRevert creates a revert for the given culprit.
   108  // Returns a revert for the culprit, created by LUCI Bisection.
   109  // Note: this should only be called according to the service-wide configuration
   110  // data for LUCI Bisection, i.e.
   111  //   - Gerrit actions are enabled
   112  //   - creating reverts is enabled
   113  //   - the daily limit of created reverts has not yet been reached
   114  func createRevert(ctx context.Context, gerritClient *gerrit.Client,
   115  	culpritModel *model.Suspect, culprit *gerritpb.ChangeInfo) (*gerritpb.ChangeInfo, error) {
   116  	revertDescription, err := generateRevertDescription(ctx, culpritModel, culprit)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	// Create the revert
   122  	revert, err := gerritClient.CreateRevert(ctx, culprit, revertDescription)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	return revert, nil
   128  }
   129  
   130  func generateRevertDescription(ctx context.Context, culpritModel *model.Suspect,
   131  	culprit *gerritpb.ChangeInfo) (string, error) {
   132  	paragraphs := []string{}
   133  
   134  	if culprit.Subject != "" {
   135  		paragraphs = append(paragraphs,
   136  			fmt.Sprintf("Revert \"%s\"", culprit.Subject))
   137  	} else {
   138  		paragraphs = append(paragraphs,
   139  			fmt.Sprintf("Revert \"%s~%d\"", culprit.Project, culprit.Number))
   140  	}
   141  
   142  	paragraphs = append(paragraphs,
   143  		fmt.Sprintf("This reverts commit %s.", culpritModel.GitilesCommit.Id))
   144  
   145  	var message string
   146  	var err error
   147  	switch culpritModel.AnalysisType {
   148  	case bisectionpb.AnalysisType_COMPILE_FAILURE_ANALYSIS:
   149  		message, err = compileFailureComment(ctx, culpritModel, "", "blameComment")
   150  		if err != nil {
   151  			return "", errors.Annotate(err, "compile failure comment").Err()
   152  		}
   153  	case bisectionpb.AnalysisType_TEST_FAILURE_ANALYSIS:
   154  		message, err = testFailureComment(ctx, culpritModel, "", "blameComment")
   155  		if err != nil {
   156  			return "", errors.Annotate(err, "test failure comment").Err()
   157  		}
   158  	}
   159  	message = "Reason for revert:\n" + message
   160  	paragraphs = append(paragraphs, message)
   161  
   162  	// Lines in the footer of the description, such as related bugs
   163  	footerLines := []string{}
   164  
   165  	// Add the culprit's description, with each line prefixed with "> "; this is
   166  	// skipped if the description for the culprit is not available.
   167  	culpritDescription, err := gerrit.CommitMessage(ctx, culprit)
   168  	if err == nil {
   169  		bugPattern := regexp.MustCompile(`(?i)^bug\s*[:=]`)
   170  		prefixedLines := []string{"Original change's description:"}
   171  		for _, line := range strings.Split(culpritDescription, "\n") {
   172  			// Check if the line specifies related bugs, so it can be used in the
   173  			// footer of the revert
   174  			if bugPattern.MatchString(line) {
   175  				footerLines = append(footerLines, line)
   176  			}
   177  
   178  			prefixedLines = append(prefixedLines,
   179  				strings.TrimSpace(fmt.Sprintf("> %s", line)))
   180  		}
   181  		paragraphs = append(paragraphs, strings.Join(prefixedLines, "\n"))
   182  	}
   183  
   184  	// Set CQ flags in the last paragraph, i.e. footer of the CL description
   185  	cqFlags := []string{
   186  		"No-Presubmit: true",
   187  		"No-Tree-Checks: true",
   188  		"No-Try: true",
   189  	}
   190  	footerLines = append(footerLines, cqFlags...)
   191  	paragraphs = append(paragraphs, strings.Join(footerLines, "\n"))
   192  
   193  	return strings.Join(paragraphs, "\n\n"), nil
   194  }