Skip to content

Commit 9037444

Browse files
authored
✨ Raw data for code review check (#1505)
* separate code review's eval and check * missing file * add comments * fix * fix * linter * fixes * fix * linter * linter * linter * draft * fixes * fixes * simplify * update date * rem comments * typo * linter * typo * linter
1 parent 7032b19 commit 9037444

File tree

9 files changed

+535
-223
lines changed

9 files changed

+535
-223
lines changed

checker/raw_result.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ type RawResults struct {
2222
SecurityPolicyResults SecurityPolicyData
2323
DependencyUpdateToolResults DependencyUpdateToolData
2424
BranchProtectionResults BranchProtectionsData
25+
CodeReviewResults CodeReviewData
26+
}
27+
28+
// CodeReviewData contains the raw results
29+
// for the Code-Review check.
30+
type CodeReviewData struct {
31+
DefaultBranchCommits []DefaultBranchCommit
2532
}
2633

2734
// VulnerabilitiesData contains the raw results
@@ -84,7 +91,7 @@ type Tool struct {
8491
Runs []Run
8592
// Issues created by the tool.
8693
Issues []Issue
87-
// Merges requests created by the tool.
94+
// Merge requests created by the tool.
8895
MergeRequests []MergeRequest
8996
Name string
9097
URL string
@@ -104,10 +111,36 @@ type Issue struct {
104111
// TODO: add fields, e.g., state=[opened|closed]
105112
}
106113

114+
// DefaultBranchCommit represents a commit
115+
// to the default branch.
116+
type DefaultBranchCommit struct {
117+
// Fields below are taken directly from cloud
118+
// version control systems, e.g. GitHub.
119+
SHA string
120+
CommitMessage string
121+
MergeRequest *MergeRequest
122+
Committer User
123+
}
124+
107125
// MergeRequest represents a merge request.
126+
//nolint:govet
108127
type MergeRequest struct {
109-
URL string
110-
// TODO: add fields, e.g., State=["merged"|"closed"]
128+
Number int
129+
Labels []string
130+
Reviews []Review
131+
Author User
132+
}
133+
134+
// Review represent a review using the built-in review system.
135+
type Review struct {
136+
Reviewer User
137+
State string
138+
// TODO(Review): add fields here if needed.
139+
}
140+
141+
// User represent a user.
142+
type User struct {
143+
Login string
111144
}
112145

113146
// File represents a file.

checks/code_review.go

Lines changed: 14 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
package checks
1616

1717
import (
18-
"fmt"
19-
"strings"
20-
2118
"github.com/ossf/scorecard/v4/checker"
19+
"github.com/ossf/scorecard/v4/checks/evaluation"
20+
"github.com/ossf/scorecard/v4/checks/raw"
2221
sce "github.com/ossf/scorecard/v4/errors"
2322
)
2423

@@ -27,196 +26,26 @@ const CheckCodeReview = "Code-Review"
2726

2827
//nolint:gochecknoinits
2928
func init() {
30-
if err := registerCheck(CheckCodeReview, DoesCodeReview); err != nil {
29+
if err := registerCheck(CheckCodeReview, CodeReview); err != nil {
3130
// this should never happen
3231
panic(err)
3332
}
3433
}
3534

36-
// DoesCodeReview attempts to determine whether a project requires review before code gets merged.
37-
// It uses a set of heuristics:
38-
// - Looking at the repo configuration to see if reviews are required.
39-
// - Checking if most of the recent merged PRs were "Approved".
40-
// - Looking for other well-known review labels.
41-
func DoesCodeReview(c *checker.CheckRequest) checker.CheckResult {
42-
// GitHub reviews.
43-
ghScore, ghReason, err := githubCodeReview(c)
44-
if err != nil {
45-
return checker.CreateRuntimeErrorResult(CheckCodeReview, err)
46-
}
47-
48-
// Review messages.
49-
hintScore, hintReason, err := commitMessageHints(c)
50-
if err != nil {
51-
return checker.CreateRuntimeErrorResult(CheckCodeReview, err)
52-
}
53-
54-
score, reason := selectBestScoreAndReason(hintScore, ghScore, hintReason, ghReason, c.Dlogger)
55-
56-
// Prow CI/CD.
57-
prowScore, prowReason, err := prowCodeReview(c)
58-
if err != nil {
59-
return checker.CreateRuntimeErrorResult(CheckCodeReview, err)
60-
}
61-
62-
score, reason = selectBestScoreAndReason(prowScore, score, prowReason, reason, c.Dlogger)
63-
if score == checker.MinResultScore {
64-
c.Dlogger.Info3(&checker.LogMessage{
65-
Text: reason,
66-
})
67-
return checker.CreateResultWithScore(CheckCodeReview, "no reviews detected", score)
68-
}
69-
70-
if score == checker.InconclusiveResultScore {
71-
return checker.CreateInconclusiveResult(CheckCodeReview, "no reviews detected")
72-
}
73-
74-
return checker.CreateResultWithScore(CheckCodeReview, checker.NormalizeReason(reason, score), score)
75-
}
76-
77-
//nolint
78-
func selectBestScoreAndReason(s1, s2 int, r1, r2 string,
79-
dl checker.DetailLogger) (int, string) {
80-
if s1 > s2 {
81-
dl.Info3(&checker.LogMessage{
82-
Text: r2,
83-
})
84-
return s1, r1
85-
}
86-
87-
dl.Info3(&checker.LogMessage{
88-
Text: r1,
89-
})
90-
return s2, r2
91-
}
92-
93-
//nolint
94-
func githubCodeReview(c *checker.CheckRequest) (int, string, error) {
95-
// Look at some merged PRs to see if they were reviewed.
96-
totalMerged := 0
97-
totalReviewed := 0
98-
prs, err := c.RepoClient.ListMergedPRs()
35+
// CodeReview will check if the maintainers perform code review.
36+
func CodeReview(c *checker.CheckRequest) checker.CheckResult {
37+
rawData, err := raw.CodeReview(c.RepoClient)
9938
if err != nil {
100-
return 0, "", sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("RepoClient.ListMergedPRs: %v", err))
39+
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
40+
return checker.CreateRuntimeErrorResult(CheckCodeReview, e)
10141
}
102-
for _, pr := range prs {
103-
if pr.MergedAt.IsZero() {
104-
continue
105-
}
106-
totalMerged++
107-
108-
// Check if the PR is approved by a reviewer.
109-
foundApprovedReview := false
110-
for _, r := range pr.Reviews {
111-
if r.State == "APPROVED" {
112-
c.Dlogger.Debug3(&checker.LogMessage{
113-
Text: fmt.Sprintf("found review approved pr: %d", pr.Number),
114-
})
115-
totalReviewed++
116-
foundApprovedReview = true
117-
break
118-
}
119-
}
120-
121-
// Check if the PR is committed by someone other than author. this is kind
122-
// of equivalent to a review and is done several times on small prs to save
123-
// time on clicking the approve button.
124-
if !foundApprovedReview &&
125-
pr.MergeCommit.Committer.Login != "" &&
126-
pr.MergeCommit.Committer.Login != pr.Author.Login {
127-
c.Dlogger.Debug3(&checker.LogMessage{
128-
Text: fmt.Sprintf("found PR#%d with committer (%s) different from author (%s)",
129-
pr.Number, pr.Author.Login, pr.MergeCommit.Committer.Login),
130-
})
131-
totalReviewed++
132-
foundApprovedReview = true
133-
}
134-
135-
if !foundApprovedReview {
136-
c.Dlogger.Debug3(&checker.LogMessage{
137-
Text: fmt.Sprintf("merged PR without code review: %d", pr.Number),
138-
})
139-
}
140-
141-
}
142-
143-
return createReturn("GitHub", totalReviewed, totalMerged)
144-
}
145-
146-
//nolint
147-
func prowCodeReview(c *checker.CheckRequest) (int, string, error) {
148-
// Look at some merged PRs to see if they were reviewed
149-
totalMerged := 0
150-
totalReviewed := 0
151-
prs, err := c.RepoClient.ListMergedPRs()
152-
if err != nil {
153-
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("RepoClient.ListMergedPRs: %v", err))
154-
}
155-
for _, pr := range prs {
156-
if pr.MergedAt.IsZero() {
157-
continue
158-
}
159-
totalMerged++
160-
for _, l := range pr.Labels {
161-
if l.Name == "lgtm" || l.Name == "approved" {
162-
totalReviewed++
163-
break
164-
}
165-
}
166-
}
167-
168-
return createReturn("Prow", totalReviewed, totalMerged)
169-
}
170-
171-
//nolint
172-
func commitMessageHints(c *checker.CheckRequest) (int, string, error) {
173-
commits, err := c.RepoClient.ListCommits()
174-
if err != nil {
175-
return checker.InconclusiveResultScore, "",
176-
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Client.Repositories.ListCommits: %v", err))
177-
}
178-
179-
total := 0
180-
totalReviewed := 0
181-
for _, commit := range commits {
182-
isBot := false
183-
committer := commit.Committer.Login
184-
for _, substring := range []string{"bot", "gardener"} {
185-
if strings.Contains(committer, substring) {
186-
isBot = true
187-
break
188-
}
189-
}
190-
if isBot {
191-
c.Dlogger.Debug3(&checker.LogMessage{
192-
Text: fmt.Sprintf("skip commit from bot account: %s", committer),
193-
})
194-
continue
195-
}
196-
197-
total++
198-
199-
// check for gerrit use via Reviewed-on and Reviewed-by
200-
commitMessage := commit.Message
201-
if strings.Contains(commitMessage, "\nReviewed-on: ") &&
202-
strings.Contains(commitMessage, "\nReviewed-by: ") {
203-
c.Dlogger.Debug3(&checker.LogMessage{
204-
Text: fmt.Sprintf("Gerrit review found for commit '%s'", commit.SHA),
205-
})
206-
totalReviewed++
207-
continue
208-
}
209-
}
210-
211-
return createReturn("Gerrit", totalReviewed, total)
212-
}
21342

214-
//nolint
215-
func createReturn(reviewName string, reviewed, total int) (int, string, error) {
216-
if total > 0 {
217-
reason := fmt.Sprintf("%s code reviews found for %v commits out of the last %v", reviewName, reviewed, total)
218-
return checker.CreateProportionalScore(reviewed, total), reason, nil
43+
// Return raw results.
44+
if c.RawResults != nil {
45+
c.RawResults.CodeReviewResults = rawData
46+
return checker.CheckResult{}
21947
}
22048

221-
return checker.InconclusiveResultScore, fmt.Sprintf("no %v commits found", reviewName), nil
49+
// Return the score evaluation.
50+
return evaluation.CodeReview(CheckCodeReview, c.Dlogger, &rawData)
22251
}

0 commit comments

Comments
 (0)