Last active
February 16, 2023 03:10
-
-
Save L1ghtmann/eb5bb1c2078be8a9af3ac487a3cf179e to your computer and use it in GitHub Desktop.
Get a list of contributors to a public GitHub org sorted by total diff
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| // | |
| // org-contributors.go | |
| // Created by Lightmann | |
| // February 2023 | |
| // | |
| import ( | |
| "os" | |
| "fmt" | |
| "sort" | |
| "bytes" | |
| "regexp" | |
| "os/exec" | |
| "strings" | |
| "strconv" | |
| "path/filepath" | |
| "github.com/jessevdk/go-flags" | |
| "golang.org/x/text/language" | |
| "golang.org/x/text/message" | |
| ) | |
| var opts struct { | |
| Ignore string `short:"i" long:"ignore" description:"Comma-separated list of repos to ignore in the organization"` | |
| Org string `short:"o" long:"org" description:"Name of GitHub organization to query" required:"true"` | |
| } | |
| func main() { | |
| flags.Parse(&opts) | |
| // https://stackoverflow.com/a/30329351 | |
| // Not off to a strong start here, GoLang .... | |
| cmd := fmt.Sprintf("curl -s \"https://api.github.com/users/%s/repos?per_page=100\" | grep -o 'git@[^\"]*'", opts.Org) | |
| cmdE := exec.Command("bash", "-c", cmd) | |
| // https://github.com/golang/go/issues/38268#issuecomment-609562062 | |
| // https://stackoverflow.com/a/75169345 | |
| // This seems harder than it has to be .... | |
| var buf bytes.Buffer | |
| cmdE.Stdout = &buf | |
| // Using the 'unused identifier' "_" to capture undesired err output | |
| _ = cmdE.Start() | |
| if err := cmdE.Wait(); err != nil { | |
| fmt.Printf("Error: the org specified -- %s -- does not appear to exist. Please check the name for typos.\n", opts.Org) | |
| return | |
| } | |
| repos := strings.Split(buf.String(),"\n") | |
| dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) | |
| tmp, _ := os.MkdirTemp("", "") | |
| os.Chdir(tmp) | |
| var authors = make(map[string]int) | |
| LOOP: for _, repo := range repos { | |
| // Sanity check | |
| if len(repo) <= 0 { | |
| continue | |
| } else { | |
| buf.Reset() | |
| } | |
| // Switch repo url to https from ssh | |
| url := strings.Replace(repo, "git@github.com:", "https://github.com/", 1) | |
| // Skip specified repos | |
| if opts.Ignore != "" { | |
| filters := []string { opts.Ignore } | |
| if strings.Contains(opts.Ignore, ",") { | |
| filters = strings.Split(opts.Ignore, ",") | |
| } | |
| for _, filter := range filters { | |
| if strings.Contains(repo, filter) { | |
| fmt.Printf("[=] Skipping %s\n", url) | |
| continue LOOP | |
| } | |
| } | |
| } | |
| fmt.Printf("[x] Starting work on %s....\n", url) | |
| // Clone repo | |
| cmd = fmt.Sprintf("git clone --quiet --no-checkout %s", url) | |
| cmdE = exec.Command("bash", "-c", cmd) | |
| _ = cmdE.Start() | |
| _ = cmdE.Wait() | |
| // Grab repo name (i.e., directory name) | |
| bits := strings.Split(url, "/") | |
| directory := strings.TrimSuffix(bits[len(bits)-1], ".git") | |
| // Get contribution info | |
| cmd = fmt.Sprintf("git -C %s --no-pager log --stat", directory) | |
| cmdE = exec.Command("bash", "-c", cmd) | |
| cmdE.Stdout = &buf | |
| _ = cmdE.Start() | |
| _ = cmdE.Wait() | |
| commits := strings.Split(buf.String(), "commit "); | |
| fmt.Println("[+] Got the commit history. Quantifying diff....") | |
| for _, commit := range commits { | |
| lines := strings.Split(commit, "\n"); | |
| // Grab relevant bits | |
| author := "" | |
| diff := "" | |
| for _, line := range lines { | |
| if strings.Contains(line, "Author: ") { | |
| author = line | |
| } else if strings.Contains(line, "changed, ") { | |
| diff = line | |
| } | |
| } | |
| // Santiy check | |
| if author == "" || diff == "" { | |
| continue | |
| } | |
| // Simplify bits | |
| author = strings.Replace(author, "Author: ", "", 1) // Remove prefix | |
| reg := regexp.MustCompile("<[^<>]*>") | |
| author = reg.ReplaceAllString(author, "${1}") // Remove email | |
| author = strings.TrimSpace(author) // Remove trailing space | |
| changes := strings.Split(diff, ", ") | |
| // Do some math | |
| for _, change := range changes { | |
| if strings.Contains(change, "-") || strings.Contains(change, "+") { | |
| reg = regexp.MustCompile("\\d") | |
| num, _ := strconv.Atoi(strings.Join(reg.FindAllString(change, -1)[:], "")) | |
| if _, exists := authors[author]; !exists { | |
| authors[author] = num | |
| } else { | |
| authors[author] += num | |
| } | |
| } | |
| } | |
| } | |
| } | |
| fmt.Println("[!] The results are in. Sorting....") | |
| // https://www.geeksforgeeks.org/how-to-sort-golang-map-by-keys-or-values/ | |
| // Sort map keys according to respective val | |
| // Need to maintain the order, hence the new keys map | |
| keys := make([]string, 0, len(authors)) | |
| for key := range authors { | |
| keys = append(keys, key) | |
| } | |
| sort.SliceStable(keys, func(i, j int) bool{ | |
| return authors[keys[i]] > authors[keys[j]] | |
| }) | |
| fmt.Printf("[-] Ordered list of contributors to %s repositories:\n", opts.Org) | |
| fmt.Println("-------------------------------------------------------") | |
| printer := message.NewPrinter(language.English) | |
| for _, author := range keys { | |
| // https://stackoverflow.com/a/46811454 | |
| // Format each diff num nicely | |
| printer.Printf("◉ %s: %d edits\n", author, authors[author]) | |
| } | |
| fmt.Println("-------------------------------------------------------") | |
| os.Chdir(dir) | |
| defer os.RemoveAll(tmp) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment