Skip to content

Commit bfc41b8

Browse files
authored
Merge pull request #21 from MadhavJivrajani/emeritus-stats
*: Add a new command emeritus-stats
2 parents 1d45bd3 + eda0eac commit bfc41b8

File tree

4 files changed

+329
-1
lines changed

4 files changed

+329
-1
lines changed

cmd/emeritus-stats.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cmd
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/kubernetes-sigs/maintainers/pkg/utils"
23+
"github.com/spf13/cobra"
24+
)
25+
26+
// emeritusStatsCmd represents the emeritus-stats command
27+
var emeritusStatsCmd = &cobra.Command{
28+
Use: "emeritus-stats",
29+
Short: "emeritus-stats gives stats on churn around emeritus_approvers in a specified time frame",
30+
Long: `emeritus-stats outputs how many emeritus_approvers or
31+
emeritus_reviewers were added or removed in a specified time frame
32+
across all OWNERS files of a specified repository.
33+
34+
Along with this, it also outputs the average number of emeritus_approvers
35+
or emeritus_reviewers added or removed per OWNERS file.`,
36+
PreRunE: func(cmd *cobra.Command, args []string) error {
37+
return flags.validate()
38+
},
39+
RunE: func(cmd *cobra.Command, args []string) (err error) {
40+
// Get the current branch in order to restore it later.
41+
currBranch, err := utils.GetBranchName(flags.dir)
42+
if err != nil {
43+
return err
44+
}
45+
46+
defer func() {
47+
err = utils.Checkout(currBranch, flags.dir)
48+
}()
49+
50+
// Checkout the repo at the from date.
51+
err = utils.CheckoutAtDate(flags.branch, flags.from, flags.dir)
52+
if err != nil {
53+
return err
54+
}
55+
56+
// Get EmeritusCounts for the from date.
57+
fromCounts, err := utils.GetEmeritusCounts(flags.dir)
58+
if err != nil {
59+
return err
60+
}
61+
62+
// Checkout the repo at the to date.
63+
err = utils.CheckoutAtDate(flags.branch, flags.to, flags.dir)
64+
if err != nil {
65+
return err
66+
}
67+
68+
// Get EmeritusCounts for the to date.
69+
toCounts, err := utils.GetEmeritusCounts(flags.dir)
70+
if err != nil {
71+
return err
72+
}
73+
74+
// Calculate the difference.
75+
diff := utils.CalculateEmeritusDiff(fromCounts, toCounts)
76+
77+
fmt.Printf("Info for the time period: %s - %s\n\n", flags.from, flags.to)
78+
fmt.Printf("For emeritus_approvers:\n\n")
79+
diff.Approvers.PrettyPrint()
80+
fmt.Printf("\nFor emeritus_reviewers:\n\n")
81+
diff.Reviewers.PrettyPrint()
82+
83+
return nil
84+
},
85+
}
86+
87+
type cmdFlags struct {
88+
from, to, dir, branch string
89+
}
90+
91+
var flags = cmdFlags{}
92+
93+
func (f cmdFlags) validate() error {
94+
if len(f.from) == 0 {
95+
return fmt.Errorf("from date needs to be specified")
96+
}
97+
if len(f.to) == 0 {
98+
return fmt.Errorf("to date needs to be specified")
99+
}
100+
if len(f.dir) == 0 {
101+
return fmt.Errorf("dir needs to be specified")
102+
}
103+
104+
return nil
105+
}
106+
107+
func init() {
108+
rootCmd.AddCommand(emeritusStatsCmd)
109+
emeritusStatsCmd.SilenceErrors = true
110+
111+
emeritusStatsCmd.Flags().StringVarP(&flags.from, "from", "f", "", "from date in format yyyy-mm-dd")
112+
emeritusStatsCmd.Flags().StringVarP(&flags.to, "to", "t", "", "to date in format yyyy-mm-dd")
113+
emeritusStatsCmd.Flags().StringVarP(&flags.dir, "dir", "d", "", "local directory where the repo is")
114+
// Defaulting to master considering this is going to be run on k/k more than other repositories.
115+
emeritusStatsCmd.
116+
Flags().
117+
StringVarP(&flags.branch, "branch", "b", "master", "base branch on which checkout should be done")
118+
}

pkg/utils/data_utils.go

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,28 @@ type OwnersInfo struct {
3030
RequiredReviewers []string `json:"required_reviewers,omitempty"`
3131
Labels []string `json:"labels,omitempty"`
3232
EmeritusApprovers []string `json:"emeritus_approvers,omitempty"`
33+
EmeritusReviewers []string `json:"emeritus_reviewers,omitempty"`
3334
Options DirOptions `json:"options,omitempty"`
3435
}
3536

37+
func (o *OwnersInfo) EmeritusApproversCount() int {
38+
count := len(o.EmeritusApprovers)
39+
for _, f := range o.Filters {
40+
count += len(f.EmeritusApprovers)
41+
}
42+
43+
return count
44+
}
45+
46+
func (o *OwnersInfo) EmeritusReviewersCount() int {
47+
count := len(o.EmeritusReviewers)
48+
for _, f := range o.Filters {
49+
count += len(f.EmeritusReviewers)
50+
}
51+
52+
return count
53+
}
54+
3655
type DirOptions struct {
3756
NoParentOwners bool `json:"no_parent_owners,omitempty"`
3857
}
@@ -42,6 +61,7 @@ type FiltersInfo struct {
4261
Reviewers []string `json:"reviewers,omitempty"`
4362
Labels []string `json:"labels,omitempty"`
4463
EmeritusApprovers []string `json:"emeritus_approvers,omitempty"`
64+
EmeritusReviewers []string `json:"emeritus_reviewers,omitempty"`
4565
RequiredReviewers []string `json:"required_reviewers,omitempty"`
4666
}
4767

@@ -88,7 +108,6 @@ type Group struct {
88108
Subprojects []Subproject `yaml:",omitempty" json:",omitempty"`
89109
}
90110

91-
92111
// PrefixToGroupMap returns a map of prefix to groups, useful for iteration over all groups
93112
func (c *Context) PrefixToGroupMap() map[string][]Group {
94113
return map[string][]Group{
@@ -190,3 +209,93 @@ func (x FoldedString) MarshalYAML() (interface{}, error) {
190209
Value: string(x),
191210
}, nil
192211
}
212+
213+
// EmeritusCounts holds mappings of path of an OWNERS file
214+
// that has emeritus_{approvers,reviewers} to how many of
215+
// them are there.
216+
type EmeritusCounts struct {
217+
ReviewerCounts map[string]int
218+
ApproverCounts map[string]int
219+
}
220+
221+
func NewEmeritusCounts() *EmeritusCounts {
222+
return &EmeritusCounts{
223+
ReviewerCounts: make(map[string]int),
224+
ApproverCounts: make(map[string]int),
225+
}
226+
}
227+
228+
// EmeritusDiff captures the values calculated as the difference
229+
// between two EmeritusCounts along with some additional info.
230+
type EmeritusDiffFields struct {
231+
AddedCount int
232+
RemovedCount int
233+
OwnersFilesWhereAdded int
234+
OwnersFilesWhereDel int
235+
AvgAddPerFile float64
236+
AvgDelPerFile float64
237+
}
238+
239+
func (d EmeritusDiffFields) PrettyPrint() {
240+
fmt.Println("Number of OWNERS files where additions were done:", d.OwnersFilesWhereAdded)
241+
fmt.Println("Number of OWNERS files where deletions were done:", d.OwnersFilesWhereDel)
242+
fmt.Println("Total number added:", d.AddedCount)
243+
fmt.Println("Total number deleted:", d.RemovedCount)
244+
fmt.Println("Avg number added per OWNERS file:", d.AvgAddPerFile)
245+
fmt.Println("Avg number deleted per OWNERS file:", d.AvgDelPerFile)
246+
}
247+
248+
type EmeritusDiff struct {
249+
Reviewers EmeritusDiffFields
250+
Approvers EmeritusDiffFields
251+
}
252+
253+
func CalculateEmeritusDiff(from, to *EmeritusCounts) EmeritusDiff {
254+
diff := EmeritusDiff{}
255+
256+
for path, count := range from.ReviewerCounts {
257+
if countTo, ok := to.ReviewerCounts[path]; ok {
258+
if countTo == count {
259+
continue
260+
}
261+
if countTo > count {
262+
diff.Reviewers.OwnersFilesWhereAdded++
263+
diff.Reviewers.AddedCount += (countTo - count)
264+
} else {
265+
diff.Reviewers.OwnersFilesWhereDel++
266+
diff.Reviewers.RemovedCount += (count - countTo)
267+
}
268+
}
269+
}
270+
271+
for path, count := range from.ApproverCounts {
272+
if countTo, ok := to.ApproverCounts[path]; ok {
273+
if countTo == count {
274+
continue
275+
}
276+
if countTo > count {
277+
diff.Approvers.OwnersFilesWhereAdded++
278+
diff.Approvers.AddedCount += (countTo - count)
279+
} else {
280+
diff.Approvers.OwnersFilesWhereDel++
281+
diff.Approvers.RemovedCount += (count - countTo)
282+
}
283+
}
284+
}
285+
286+
if diff.Reviewers.OwnersFilesWhereAdded != 0 {
287+
diff.Reviewers.AvgAddPerFile = float64(diff.Reviewers.AddedCount) / float64(diff.Reviewers.OwnersFilesWhereAdded)
288+
}
289+
if diff.Reviewers.OwnersFilesWhereDel != 0 {
290+
diff.Reviewers.AvgDelPerFile = float64(diff.Reviewers.RemovedCount) / float64(diff.Reviewers.OwnersFilesWhereDel)
291+
}
292+
293+
if diff.Approvers.OwnersFilesWhereAdded != 0 {
294+
diff.Approvers.AvgAddPerFile = float64(diff.Approvers.AddedCount) / float64(diff.Approvers.OwnersFilesWhereAdded)
295+
}
296+
if diff.Approvers.OwnersFilesWhereDel != 0 {
297+
diff.Approvers.AvgDelPerFile = float64(diff.Approvers.RemovedCount) / float64(diff.Approvers.OwnersFilesWhereDel)
298+
}
299+
300+
return diff
301+
}

pkg/utils/file_utils.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,22 @@ func GetSigsYamlFile(root string) (string, error) {
113113
}
114114
return "", err
115115
}
116+
117+
func GetEmeritusCounts(path string) (*EmeritusCounts, error) {
118+
ownerFiles, err := GetOwnerFiles(path)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
counts := NewEmeritusCounts()
124+
for _, file := range ownerFiles {
125+
info, err := GetOwnersInfo(file)
126+
if err != nil {
127+
return nil, err
128+
}
129+
counts.ReviewerCounts[file] = info.EmeritusReviewersCount()
130+
counts.ApproverCounts[file] = info.EmeritusApproversCount()
131+
}
132+
133+
return counts, nil
134+
}

pkg/utils/git_utils.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package utils
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"os/exec"
23+
)
24+
25+
// CheckoutAtDate checks out the commit at the specified date.
26+
func CheckoutAtDate(branch, date, dir string) error {
27+
err := os.Chdir(dir)
28+
if err != nil {
29+
return err
30+
}
31+
32+
cmd := exec.Command(
33+
"/bin/bash",
34+
"-c",
35+
fmt.Sprintf("git checkout `git rev-list -n 1 --before=\"%s\" %s`", date, branch),
36+
)
37+
38+
if err := cmd.Run(); err != nil {
39+
return err
40+
}
41+
42+
return nil
43+
}
44+
45+
func GetBranchName(dir string) (string, error) {
46+
err := os.Chdir(dir)
47+
if err != nil {
48+
return "", err
49+
}
50+
51+
cmd := exec.Command(
52+
"/bin/bash",
53+
"-c",
54+
"git rev-parse --abbrev-ref HEAD",
55+
)
56+
57+
bytes, err := cmd.Output()
58+
if err != nil {
59+
return "", err
60+
}
61+
62+
return string(bytes), nil
63+
}
64+
65+
func Checkout(branch, dir string) error {
66+
err := os.Chdir(dir)
67+
if err != nil {
68+
return err
69+
}
70+
71+
cmd := exec.Command(
72+
"/bin/bash",
73+
"-c",
74+
fmt.Sprintf("git checkout %s", branch),
75+
)
76+
77+
if err := cmd.Run(); err != nil {
78+
return err
79+
}
80+
81+
return nil
82+
}

0 commit comments

Comments
 (0)