go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/usertext/cl_error.go (about)

     1  // Copyright 2021 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 usertext
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"strings"
    22  	"text/template"
    23  
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/common/retry/transient"
    26  	"go.chromium.org/luci/gae/service/datastore"
    27  
    28  	"go.chromium.org/luci/cv/internal/changelist"
    29  	"go.chromium.org/luci/cv/internal/common"
    30  	"go.chromium.org/luci/cv/internal/run"
    31  )
    32  
    33  // SFormatCLError is the same as FormatCLError but returns a string.
    34  func SFormatCLError(ctx context.Context, reason *changelist.CLError, cl *changelist.CL, mode run.Mode) (string, error) {
    35  	var sb strings.Builder
    36  	if err := FormatCLError(ctx, reason, cl, mode, &sb); err != nil {
    37  		return "", err
    38  	}
    39  	return sb.String(), nil
    40  }
    41  
    42  // FormatCLError formats 1 CL error by writing it into strings.Builder.
    43  //
    44  // mode should be specified if known.
    45  func FormatCLError(ctx context.Context, reason *changelist.CLError, cl *changelist.CL, mode run.Mode, sb *strings.Builder) error {
    46  	switch v := reason.GetKind().(type) {
    47  	case *changelist.CLError_OwnerLacksEmail:
    48  		if !v.OwnerLacksEmail {
    49  			return errors.New("owner_lacks_email must be set to true")
    50  		}
    51  		return tmplCLOwnerLacksEmails.Execute(sb, map[string]string{
    52  			"GerritHost": cl.Snapshot.GetGerrit().GetHost(),
    53  		})
    54  
    55  	case *changelist.CLError_UnsupportedMode:
    56  		if v.UnsupportedMode == "" {
    57  			return errors.New("unsupported_mode must be set")
    58  		}
    59  		return tmplUnsupportedMode.Execute(sb, v)
    60  
    61  	case *changelist.CLError_SelfCqDepend:
    62  		if !v.SelfCqDepend {
    63  			return errors.New("self_cq_depend must be set")
    64  		}
    65  		return tmplSelfCQDepend.Execute(sb, nil)
    66  
    67  	case *changelist.CLError_CorruptGerritMetadata:
    68  		if v.CorruptGerritMetadata == "" {
    69  			return errors.New("corrupt_gerrit_metadata must be set")
    70  		}
    71  		return tmplCorruptGerritCLMetadata.Execute(sb, v)
    72  
    73  	case *changelist.CLError_WatchedByManyConfigGroups_:
    74  		cgs := v.WatchedByManyConfigGroups.GetConfigGroups()
    75  		if len(cgs) < 2 {
    76  			return errors.New("at least 2 config_groups required")
    77  		}
    78  		return tmplWatchedByManyConfigGroups.Execute(sb, map[string]any{
    79  			"ConfigGroups": cgs,
    80  			"TargetRef":    cl.Snapshot.GetGerrit().GetInfo().GetRef(),
    81  		})
    82  
    83  	case *changelist.CLError_WatchedByManyProjects_:
    84  		projects := v.WatchedByManyProjects.GetProjects()
    85  		if len(projects) < 2 {
    86  			return errors.New("at least 2 projects required")
    87  		}
    88  		return tmplWatchedByManyProjects.Execute(sb, map[string]any{
    89  			"Projects": projects,
    90  		})
    91  
    92  	case *changelist.CLError_InvalidDeps_:
    93  		// Although it's possible for a CL to have several kinds of wrong deps,
    94  		// it's rare in practice, so simply error out on the most important kind.
    95  		var bad []*changelist.Dep
    96  		args := make(map[string]any, 2)
    97  		var t *template.Template
    98  		switch d := v.InvalidDeps; {
    99  		case len(d.GetUnwatched()) > 0:
   100  			bad, t = d.GetUnwatched(), tmplUnwatchedDeps
   101  		case len(d.GetWrongConfigGroup()) > 0:
   102  			bad, t = d.GetWrongConfigGroup(), tmplWrongDepsConfigGroup
   103  		case len(d.GetSingleFullDeps()) > 0:
   104  			bad, t = d.GetSingleFullDeps(), tmplSingleFullOpenDeps
   105  			args["mode"] = string(mode)
   106  		case len(d.GetCombinableUntriggered()) > 0:
   107  			bad, t = d.GetCombinableUntriggered(), tmplCombinableUntriggered
   108  		case len(d.GetCombinableMismatchedMode()) > 0:
   109  			bad, t = d.GetCombinableMismatchedMode(), tmplCombinableMismatchedMode
   110  			args["mode"] = string(mode)
   111  		case d.GetTooMany() != nil:
   112  			args["max"] = d.GetTooMany().GetMaxAllowed()
   113  			args["actual"] = d.GetTooMany().GetActual()
   114  			t = tmplTooManyDeps
   115  			bad = nil // there is no point listing all of them.
   116  		default:
   117  			return errors.Reason("unsupported InvalidDeps reason %s", d).Err()
   118  		}
   119  		urls, err := depsURLs(ctx, bad)
   120  		if err != nil {
   121  			return err
   122  		}
   123  		sort.Strings(urls)
   124  		args["deps"] = urls
   125  		return t.Execute(sb, args)
   126  
   127  	case *changelist.CLError_ReusedTrigger_:
   128  		if v.ReusedTrigger == nil {
   129  			return errors.New("reused_trigger must be set")
   130  		}
   131  		return tmplReusedTrigger.Execute(sb, v.ReusedTrigger)
   132  
   133  	case *changelist.CLError_CommitBlocked:
   134  		return tmplCommitBlocked.Execute(sb, nil)
   135  
   136  	case *changelist.CLError_TriggerDeps_:
   137  		if v.TriggerDeps == nil {
   138  			return errors.New("trigger_deps must be set")
   139  		}
   140  		var msgs []string
   141  		var deps []*changelist.Dep
   142  		for _, denied := range v.TriggerDeps.GetPermissionDenied() {
   143  			var mb strings.Builder
   144  			fmt.Fprintf(&mb, "no permission to vote")
   145  			if denied.GetEmail() != "" {
   146  				fmt.Fprintf(&mb, " on behalf of %s", denied.GetEmail())
   147  			}
   148  			msgs = append(msgs, mb.String())
   149  			deps = append(deps, &changelist.Dep{Clid: denied.GetClid()})
   150  		}
   151  		for _, clid := range v.TriggerDeps.GetNotFound() {
   152  			msgs = append(msgs, "the CL no longer exists in Gerrit")
   153  			deps = append(deps, &changelist.Dep{Clid: clid})
   154  		}
   155  		for _, clid := range v.TriggerDeps.GetInternalGerritError() {
   156  			msgs = append(msgs, "internal Gerrit error")
   157  			deps = append(deps, &changelist.Dep{Clid: clid})
   158  		}
   159  		urls, err := depsURLs(ctx, deps)
   160  		if err != nil {
   161  			return err
   162  		}
   163  		return tmplTriggerDeps.Execute(sb, map[string]any{"urls": urls, "messages": msgs})
   164  
   165  	case *changelist.CLError_DepRunFailed:
   166  		if v.DepRunFailed == 0 {
   167  			return errors.New("dep_run_failed must be > 0")
   168  		}
   169  		urls, err := depsURLs(ctx, []*changelist.Dep{{Clid: v.DepRunFailed}})
   170  		if err != nil {
   171  			return err
   172  		}
   173  		return tmplDepRunFailed.Execute(sb, map[string]any{"url": urls[0]})
   174  
   175  	default:
   176  		return errors.Reason("unsupported purge reason %t: %s", v, reason).Err()
   177  	}
   178  }
   179  
   180  func depsURLs(ctx context.Context, deps []*changelist.Dep) ([]string, error) {
   181  	cls := make([]*changelist.CL, len(deps))
   182  	for i, d := range deps {
   183  		cls[i] = &changelist.CL{ID: common.CLID(d.GetClid())}
   184  	}
   185  	if err := datastore.Get(ctx, cls); err != nil {
   186  		return nil, errors.Annotate(err, "failed to load deps as CLs").Tag(transient.Tag).Err()
   187  	}
   188  	urls := make([]string, len(deps))
   189  	for i, cl := range cls {
   190  		var err error
   191  		if urls[i], err = cl.URL(); err != nil {
   192  			return nil, err
   193  		}
   194  	}
   195  	return urls, nil
   196  }
   197  
   198  var tmplCLOwnerLacksEmails = tmplMust(`
   199  {{CQ_OR_CV}} can't process the CL because its owner doesn't have a preferred email set in Gerrit settings.
   200  
   201  You can set preferred email at https://{{.GerritHost}}/settings/#EmailAddresses
   202  `)
   203  
   204  var tmplUnsupportedMode = tmplMust(`
   205  {{CQ_OR_CV}} can't process the CL because its mode {{.UnsupportedMode | printf "%q"}} is not supported.
   206  {{CONTACT_YOUR_INFRA}}
   207  `)
   208  
   209  var tmplSelfCQDepend = tmplMust(`
   210  {{CQ_OR_CV}} can't process the CL because it depends on itself.
   211  
   212  Please check Cq-Depend: in CL description (commit message). If you think this is a mistake, {{CONTACT_YOUR_INFRA}}.
   213  `)
   214  
   215  var tmplCorruptGerritCLMetadata = tmplMust(`
   216  {{CQ_OR_CV}} can't process the CL because its Gerrit metadata looks corrupted.
   217  
   218  {{.CorruptGerritMetadata}}
   219  
   220  Consider filing a Gerrit bug or {{CONTACT_YOUR_INFRA}}.
   221  In the meantime, consider re-uploading your CL(s).
   222  `)
   223  
   224  var tmplWatchedByManyConfigGroups = tmplMust(`
   225  {{CQ_OR_CV}} can't process the CL because it is watched by more than 1 config group:
   226  {{range $cg := .ConfigGroups}}  * {{$cg}}
   227  {{end}}
   228  {{CONTACT_YOUR_INFRA}}. For their info:
   229    * current CL target ref is {{.TargetRef | printf "%q"}},
   230  	* relevant doc https://chromium.googlesource.com/infra/luci/luci-go/+/HEAD/lucicfg/doc/#luci.cq_group
   231  `)
   232  
   233  var tmplWatchedByManyProjects = tmplMust(`
   234  {{CQ_OR_CV}} can't process the CL because it is watched by more than 1 LUCI project:
   235  {{range $p := .Projects}}  * {{$p}}
   236  {{end}}
   237  {{CONTACT_YOUR_INFRA}}. Relevant doc for their info:
   238  https://chromium.googlesource.com/infra/luci/luci-go/+/HEAD/lucicfg/doc/#luci.cq_group
   239  `)
   240  
   241  var tmplUnwatchedDeps = tmplMust(`
   242  {{CQ_OR_CV}} can't process the CL because its deps are not watched by the same LUCI project:
   243  {{range $url := .deps}}  * {{$url}}
   244  {{end}}
   245  Please check Cq-Depend: in CL description (commit message). If you think this is a mistake, {{CONTACT_YOUR_INFRA}}.
   246  `)
   247  
   248  var tmplWrongDepsConfigGroup = tmplMust(`
   249  {{CQ_OR_CV}} can't process the CL because its deps do not belong to the same config group:
   250  {{range $url := .deps}}  * {{$url}}
   251  {{end}}
   252  `)
   253  
   254  var tmplSingleFullOpenDeps = tmplMust(`
   255  {{CQ_OR_CV}} can't process the CL in {{.mode | printf "%q"}} mode because it has not yet submitted dependencies:
   256  {{range $url := .deps}}  * {{$url}}
   257  {{end}}
   258  Please submit directly or via CQ the dependencies first.
   259  `)
   260  
   261  var tmplCombinableUntriggered = tmplMust(`
   262  {{CQ_OR_CV}} can't process the CL because its dependencies weren't CQ-ed at all:
   263  {{range $url := .deps}}  * {{$url}}
   264  {{end}}
   265  Please trigger this CL and its dependencies at the same time.
   266  `)
   267  
   268  var tmplCombinableMismatchedMode = tmplMust(`
   269  {{CQ_OR_CV}} can't process the CL because its mode {{.mode | printf "%q"}} does not match mode on its dependencies:
   270  {{range $url := .deps}}  * {{$url}}
   271  {{end}}
   272  `)
   273  
   274  var tmplTooManyDeps = tmplMust(`
   275  {{CQ_OR_CV}} can't process the CL because it has too many deps: {{.actual}} (max supported: {{.max}})
   276  `)
   277  
   278  // TODO(crbug/1223350): once we have a way to reference prior Runs, add a link
   279  // to the Run.
   280  var tmplReusedTrigger = tmplMust(`
   281  {{CQ_OR_CV}} can't process the CL because it has previously completed a Run ({{.Run | printf "%q"}}) triggered by the same vote(s).
   282  
   283  This may happen in rare circumstances such as moving a Gerrit Change to a new branch, abandoning & restoring the CL during an ongoing CQ Run, or when different users vote differently on a CQ label.
   284  
   285  Please re-trigger the CQ if necessary.
   286  `)
   287  
   288  var tmplCommitBlocked = tmplMust(`
   289  {{CQ_OR_CV}} won't start a full run for the CL because it has a "Commit: false" footer.
   290  
   291  The "Commit: false" footer is used to prevent accidental submission of a CL. You may try a dry run; if you want to submit the CL, you must first remove the "Commit: false" footer from the CL description.
   292  `)
   293  
   294  var tmplTriggerDeps = tmplMust(`
   295  {{CQ_OR_CV}} failed to vote the CQ label on the following dependencies.
   296  {{range $i, $url := .urls}}  * {{$url}} - {{index $.messages $i}}
   297  {{end}}
   298  `)
   299  
   300  var tmplDepRunFailed = tmplMust(`
   301  Revoking the CQ vote, because a Run failed on [CL]({{.url}}) that this CL depends on.
   302  `)