blob: 9e610e0261cc2e06b46ab03183b48b94a0cabe6f [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package reviews
import (
"fmt"
"sync"
)
// A Predicate is a categorization of a [Change].
// An example of a Predicate would be "has been approved by a maintainer"
// or "author is a known contributor" or
// "waiting for response from author."
// The dashboard permits filtering and sorting CLs based on the different
// predicates they satisfy.
// A Predicate has a default score that indicates how important it is,
// but the dashboard permits using different sort orders.
type Predicate struct {
Name string // a short, one-word name
Description string // a longer description
Score int // default scoring value
// The Applies function reports whether this Predicate
// applies to a change.
Applies func(Change) (bool, error)
}
// ChangePreds is a [Change] with a list of predicates that apply
// to that change.
type ChangePreds struct {
Change Change
Predicates []*Predicate
}
// A Reject is like a [Predicate], but if the Reject applies to a [Change]
// then the Change is not put on the dashboard at all.
type Reject Predicate
// ApplyPredicates takes a [Change] and applies predicates to it.
// The bool result reports whether the change is reviewable;
// this will be false if the change should not be reviewed,
// for example because it has already been committed.
func ApplyPredicates(change Change) (ChangePreds, bool, error) {
predicatesLock.Lock()
// The predicates and rejects slices are append-only,
// so a top-level copy is safe to use.
p := predicates
r := rejects
predicatesLock.Unlock()
for i := range r {
applies, err := r[i].Applies(change)
if err != nil {
return ChangePreds{}, false, err
}
if applies {
return ChangePreds{}, false, nil
}
}
var preds []*Predicate
for i := range p {
pred := &p[i]
applies, err := pred.Applies(change)
if err != nil {
return ChangePreds{}, false, err
}
if applies {
preds = append(preds, pred)
}
}
cp := ChangePreds{
Change: change,
Predicates: preds,
}
return cp, true, nil
}
// AddPredicates adds more [Predicate] values for a project.
func AddPredicates(newPreds []Predicate) {
predicatesLock.Lock()
defer predicatesLock.Unlock()
predicates = append(predicates, newPreds...)
}
// AddRejects adds more [Reject] values for a project.
func AddRejects(newRejects []Reject) {
predicatesLock.Lock()
defer predicatesLock.Unlock()
rejects = append(rejects, newRejects...)
}
// Some [Predicate] default scores.
const (
ScoreImportant = 10 // change is important
ScoreSuggested = 1 // change is worth looking at
ScoreUninteresting = -1 // change is not interesting
ScoreUnimportant = -10 // change is not important
)
// predicatesLock protects predicates and rejectors.
var predicatesLock sync.Mutex
// predicates is the list of [Predicate] values that we apply to a change.
var predicates = []Predicate{
{
Name: "authorMaintainer",
Description: "the change author is a project maintainer",
Score: ScoreImportant,
Applies: authorMaintainer,
},
{
Name: "authorReviewer",
Description: "the change author is a project reviewer",
Score: ScoreSuggested,
Applies: authorReviewer,
},
{
Name: "authorContributor",
Description: "the change author is a project contributor",
Score: ScoreSuggested,
Applies: authorContributor,
},
{
Name: "authorMajorContributor",
Description: "the change author is a major project contributor",
Score: ScoreImportant,
Applies: authorMajorContributor,
},
{
Name: "noMaintainerReviews",
Description: "has no reviews from a project maintainer",
Score: ScoreSuggested,
Applies: noMaintainerReviews,
},
}
// authorMaintainer is a [Predicate] function that reports whether the
// [Change] author is a project maintainer.
func authorMaintainer(ch Change) (bool, error) {
switch ch.Author().Authority() {
case AuthorityMaintainer, AuthorityOwner:
return true, nil
default:
return false, nil
}
}
// authorReviewer is a [Predicate] function that reports whether the
// [Change] author is a project reviewer.
func authorReviewer(ch Change) (bool, error) {
switch ch.Author().Authority() {
case AuthorityReviewer:
return true, nil
default:
return false, nil
}
}
// authorContributor is a [Predicate] function that reports whether the
// [Change] author is a known contributor: more than 10 changes contributed.
func authorContributor(ch Change) (bool, error) {
return ch.Author().Commits() > 10, nil
}
// authorMajorContributor is a [Predicate] function that reports whether the
// [Change] author is a major contributor: more than 50 changes contributed.
func authorMajorContributor(ch Change) (bool, error) {
return ch.Author().Commits() > 50, nil
}
// noMaintainerReviews is a [Predicate] function that reports whether the
// [Change] has not been reviewed by a maintainer.
func noMaintainerReviews(ch Change) (bool, error) {
for _, r := range ch.Reviewed() {
switch r.Authority() {
case AuthorityMaintainer, AuthorityOwner:
return false, nil
}
}
return true, nil
}
// rejects is the list of Reject values that we apply to a change.
var rejects = []Reject{
{
Name: "reviewable",
Description: "whether the change is reviewable",
Applies: unreviewable,
},
}
// unreviewable is a [Reject] function that reports whether a
// [Change] is not reviewable.
func unreviewable(ch Change) (bool, error) {
switch status := ch.Status(); status {
case StatusReady:
return false, nil
case StatusSubmitted, StatusClosed, StatusDoNotReview:
return true, nil
default:
return false, fmt.Errorf("reviewable predicate: change %s: unrecognized status %d", ch.ID(), status)
}
}