From b1f40904dfe1eaf12f3959ac5c80b9e748a240d6 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sat, 18 Jun 2022 14:31:41 -0700 Subject: 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 --- tools/git-blame-stats/default.nix | 15 +++++++ tools/git-blame-stats/go.mod | 3 ++ tools/git-blame-stats/main.go | 86 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 tools/git-blame-stats/default.nix create mode 100644 tools/git-blame-stats/go.mod create mode 100644 tools/git-blame-stats/main.go 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 +} -- cgit 1.4.1