about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2022-06-18 14:31:41 -0700
committerFranck Cuny <franck@fcuny.net>2022-06-18 14:38:19 -0700
commitb1f40904dfe1eaf12f3959ac5c80b9e748a240d6 (patch)
treeba28225679fb27509f6769d4be2e4d8a0a80eb31
parentref(gerrit): add the plugin to delete projects (diff)
downloadworld-b1f40904dfe1eaf12f3959ac5c80b9e748a240d6.tar.gz
feat(tools/git-blame-stats): add a CLI to report git blame statistics
Running this command in a repository will report how many lines in the
current paths were created by different author, for a given revision. If
no revision is specific, HEAD is assumed.

Change-Id: I3fbed4f35a05e12fef22a72d7231727c05c50c96
Reviewed-on: https://cl.fcuny.net/c/world/+/445
Tested-by: CI
Reviewed-by: Franck Cuny <franck@fcuny.net>
-rw-r--r--tools/git-blame-stats/default.nix15
-rw-r--r--tools/git-blame-stats/go.mod3
-rw-r--r--tools/git-blame-stats/main.go86
3 files changed, 104 insertions, 0 deletions
diff --git a/tools/git-blame-stats/default.nix b/tools/git-blame-stats/default.nix
new file mode 100644
index 0000000..8897b67
--- /dev/null
+++ b/tools/git-blame-stats/default.nix
@@ -0,0 +1,15 @@
+{ pkgs, ... }:
+
+pkgs.buildGoModule rec {
+  name = "git-blame-stats";
+  src = ./.;
+  vendorSha256 = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo=";
+  nativeBuildInputs = with pkgs; [ go ];
+
+  meta = with pkgs.lib; {
+    description = "CLI to reports git blame statistics per author.";
+    license = licenses.mit;
+    platforms = platforms.linux;
+    maintainers = [ ];
+  };
+}
diff --git a/tools/git-blame-stats/go.mod b/tools/git-blame-stats/go.mod
new file mode 100644
index 0000000..4738ac4
--- /dev/null
+++ b/tools/git-blame-stats/go.mod
@@ -0,0 +1,3 @@
+module golang.fcuny.net/git-blame-stats
+
+go 1.17
diff --git a/tools/git-blame-stats/main.go b/tools/git-blame-stats/main.go
new file mode 100644
index 0000000..8b1bc9a
--- /dev/null
+++ b/tools/git-blame-stats/main.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"regexp"
+	"sort"
+	"strings"
+)
+
+func main() {
+	rev := "HEAD"
+	if len(os.Args) == 2 {
+		rev = os.Args[1]
+	}
+
+	files := gitListFiles(rev)
+
+	authors := gitBlameFiles(rev, files)
+
+	sortedAuthors, keys := sortAuthors(authors)
+
+	rank := 1
+
+	for _, k := range keys {
+		for i := 0; i < len(sortedAuthors[k]); i++ {
+			fmt.Printf("%3d - %6d %s\n", rank, k, sortedAuthors[k][i])
+			rank = rank + 1
+		}
+	}
+}
+
+func gitListFiles(rev string) []string {
+	out, err := exec.Command("git", "ls-tree", "--name-only", "-r", rev).Output()
+	if err != nil {
+		log.Fatal(err)
+	}
+	files := strings.Split(string(out), "\n")
+	return files
+}
+
+func gitBlameFiles(rev string, files []string) map[string]int {
+	authors := make(map[string]int)
+
+	for i := 0; i < len(files)-1; i++ {
+		gitBlameFile(rev, files[i], authors)
+	}
+	return authors
+}
+
+func gitBlameFile(rev, file string, authors map[string]int) {
+	out, err := exec.Command("git", "blame", "-e", "-w", rev, "--", file).Output()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	lines := strings.Split(string(out), "\n")
+
+	authorRegex := regexp.MustCompile(`^.*?\((.*?)\s*\d{4}-\d{2}-\d{2}.*`)
+
+	for j := 0; j < len(lines)-1; j++ {
+		if string(lines[j][0]) != "^" {
+			matched := authorRegex.FindStringSubmatch(string(lines[j]))
+			if len(matched) > 0 {
+				authors[matched[1]] = authors[matched[1]] + 1
+			}
+		}
+	}
+}
+
+func sortAuthors(authors map[string]int) (map[int][]string, []int) {
+	var keys []int
+	sortedAuthors := make(map[int][]string)
+
+	for k, v := range authors {
+		sortedAuthors[v] = append(sortedAuthors[v], k)
+		if len(sortedAuthors[v]) == 1 {
+			keys = append(keys, v)
+		}
+	}
+	sort.Sort(sort.Reverse(sort.IntSlice(keys)))
+
+	return sortedAuthors, keys
+}