aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2025-08-25 07:46:47 -0700
committerFranck Cuny <franck@fcuny.net>2025-08-25 07:46:47 -0700
commit977190e31f91339dae91253f53af89c38cbe04fc (patch)
treebd942f1ebffc955be2caa724abc55e6703993639
parentadd seq-stat: terminal histogram generator for number sequences (diff)
downloadx-977190e31f91339dae91253f53af89c38cbe04fc.tar.gz
add `git-leaderboard`
Diffstat (limited to '')
-rw-r--r--cmd/git-leaderboard/main.go128
1 files changed, 128 insertions, 0 deletions
diff --git a/cmd/git-leaderboard/main.go b/cmd/git-leaderboard/main.go
new file mode 100644
index 0000000..7c846a5
--- /dev/null
+++ b/cmd/git-leaderboard/main.go
@@ -0,0 +1,128 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "os/exec"
+ "sort"
+ "strings"
+)
+
+type Author struct {
+ Name string
+ Count int
+}
+
+func main() {
+ lineFlag := flag.Bool("line", false, "Count lines changed instead of commits")
+ flag.Parse()
+
+ if !isGitRepository() {
+ fmt.Println("Error: Not in a git repository")
+ os.Exit(1)
+ }
+
+ authors, err := getAuthors(*lineFlag)
+ if err != nil {
+ fmt.Printf("Error getting authors: %v\n", err)
+ os.Exit(1)
+ }
+
+ if len(authors) == 0 {
+ fmt.Println("No authors found. The repository might be empty or have no commits.")
+ os.Exit(0)
+ }
+
+ sort.Slice(authors, func(i, j int) bool {
+ return authors[i].Count > authors[j].Count
+ })
+
+ maxCount := authors[0].Count
+
+ fmt.Printf("%-30s %-10s %s\n", "Author", "Count", "Contribution")
+ fmt.Println(strings.Repeat("-", 80))
+
+ for _, author := range authors {
+ bar := generateBar(author.Count, maxCount)
+ fmt.Printf("%-30s %-10d %s\n", author.Name, author.Count, bar)
+ }
+}
+
+func isGitRepository() bool {
+ cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree")
+ err := cmd.Run()
+ return err == nil
+}
+
+func getAuthors(countLines bool) ([]Author, error) {
+ var cmd *exec.Cmd
+
+ if countLines {
+ cmd = exec.Command("git", "log", "--pretty=format:%aN", "--numstat")
+ } else {
+ cmd = exec.Command("git", "shortlog", "-sn", "--no-merges", "HEAD")
+ }
+
+ output, err := cmd.Output()
+ if err != nil {
+ if exitErr, ok := err.(*exec.ExitError); ok {
+ return nil, fmt.Errorf("git command failed: %s. Error output: %s", err, exitErr.Stderr)
+ }
+ return nil, fmt.Errorf("error executing git command: %v", err)
+ }
+
+ lines := strings.Split(string(output), "\n")
+ authors := make(map[string]int)
+
+ if countLines {
+ currentAuthor := ""
+ for _, line := range lines {
+ if line == "" {
+ currentAuthor = ""
+ continue
+ }
+ if currentAuthor == "" {
+ currentAuthor = line
+ continue
+ } else {
+ fields := strings.Fields(line)
+ if len(fields) >= 2 {
+ added, _ := parseInt(fields[0])
+ deleted, _ := parseInt(fields[1])
+ authors[currentAuthor] += added + deleted
+ }
+ }
+ }
+ } else {
+ for _, line := range lines {
+ fields := strings.Fields(line)
+ if len(fields) >= 2 {
+ count, _ := parseInt(fields[0])
+ name := strings.Join(fields[1:], " ")
+ authors[name] = count
+ }
+ }
+ }
+
+ var result []Author
+ for name, count := range authors {
+ result = append(result, Author{Name: name, Count: count})
+ }
+ return result, nil
+}
+
+func parseInt(s string) (int, error) {
+ if s == "-" {
+ return 0, nil
+ }
+ var result int
+ _, err := fmt.Sscanf(s, "%d", &result)
+ return result, err
+}
+
+func generateBar(count, maxCount int) string {
+ maxWidth := 38
+ width := int(float64(count) / float64(maxCount) * float64(maxWidth))
+ return strings.Repeat("▆", width)
+}