Skip to content

Instantly share code, notes, and snippets.

@L1ghtmann
Last active February 16, 2023 03:10
Show Gist options
  • Select an option

  • Save L1ghtmann/eb5bb1c2078be8a9af3ac487a3cf179e to your computer and use it in GitHub Desktop.

Select an option

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
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