Skip to content

Commit eda0eac

Browse files
*: Add a new command emeritus-stats
emeritus-stats gives how many emeritus_reviewers or emeritus_approvers were added and removed in a given time frame for a particular repository. This commit adds the relevant helper functions to calculate these numbers are print them. This commit also introduces new types to facilitate this calculation as well as edits existing types if needed. The high level algorithm followed is: - Checkout the repo at `path` at date `from` - Collect numbers for `emeritus_reviewers` and `emeritus_approvers` for `from` - Checkout the repo at `path` at date `to` - Collect similar number for `to` - Calculate the difference between these two Signed-off-by: Madhav Jivrajani <[email protected]>
1 parent 7757ffc commit eda0eac

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)