about summary refs log tree commit diff
path: root/tools/git-blame-stats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tools/git-blame-stats/default.nix24
-rwxr-xr-xtools/git-blame-stats/git-blame-stats.py95
-rw-r--r--tools/git-blame-stats/go.mod3
-rw-r--r--tools/git-blame-stats/main.go86
4 files changed, 112 insertions, 96 deletions
diff --git a/tools/git-blame-stats/default.nix b/tools/git-blame-stats/default.nix
index 5071f10..767329b 100644
--- a/tools/git-blame-stats/default.nix
+++ b/tools/git-blame-stats/default.nix
@@ -1,15 +1,25 @@
-{ pkgs, buildGoModule, ... }:
+{ self, lib, python3, stdenvNoCC, pkgs }:
+
+stdenvNoCC.mkDerivation rec {
+  pname = "git-blame-stats";
+  src = ./git-blame-stats.py;
+  version = "0.1.1";
+
+  nativeBuildInputs = with pkgs; [ python3 ];
+
+  dontUnpack = true;
+  dontBuild = true;
+
+  installPhase = ''
+    mkdir -p $out/bin
+    cp $src $out/bin/${pname}
+  '';
 
-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;
+    platforms = platforms.unix;
     maintainers = [ ];
   };
 }
diff --git a/tools/git-blame-stats/git-blame-stats.py b/tools/git-blame-stats/git-blame-stats.py
new file mode 100755
index 0000000..ee52ce4
--- /dev/null
+++ b/tools/git-blame-stats/git-blame-stats.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+
+import argparse
+import subprocess
+import sys
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+    "rev", metavar="revision", type=str, help="the revision", default="HEAD", nargs="?"
+)
+args = parser.parse_args()
+
+authors = dict()
+max_lenght_author = 0
+max_lenght_email = 0
+
+
+def get_files(rev):
+    """Returns a list of files for the repository, at the given path, for the given revision."""
+    tree = subprocess.run(
+        ["git", "ls-tree", "--name-only", "-r", rev],
+        capture_output=True,
+        check=True,
+        encoding="utf-8",
+    )
+    return tree.stdout.splitlines()
+
+
+def line_info(filename, rev):
+    """Generates a set of commit blocks using `git blame` for a file.
+
+    Each block corresponds to the information about a single line of code."""
+    blame = subprocess.run(
+        ["git", "blame", "-w", "--line-porcelain", rev, "--", filename],
+        capture_output=True,
+        encoding="utf-8",
+        check=True,
+    )
+    block = []
+    for line in blame.stdout.splitlines():
+        block.append(line)
+        if line.startswith("\t"):
+            yield block
+            block = []
+
+
+files = get_files(args.rev)
+
+for filename in files:
+    try:
+        for block in line_info(filename.rstrip(), args.rev):
+            author = None
+            author_email = None
+            commit = None
+            skip = False
+            for i, val in enumerate(block):
+                if i == 0:
+                    commit = val.split()[0]
+                    continue
+                if val.startswith("author "):
+                    author = " ".join(val.split()[1:])
+                    continue
+                if val.startswith("author-mail"):
+                    author_email = " ".join(val.split()[1:])
+                    continue
+                if val.startswith("\t") and val == "\t":
+                    skip == True
+            if skip:
+                continue
+            if authors.get(author, None) == None:
+                authors[author] = {
+                    "email": author_email,
+                    "commits": set(),
+                    "files": set(),
+                    "lines": 0,
+                }
+            authors[author]["commits"].add(commit)
+            authors[author]["files"].add(filename)
+            authors[author]["lines"] += 1
+            if len(author) > max_lenght_author:
+                max_lenght_author = len(author)
+            if len(author_email) > max_lenght_email:
+                max_lenght_email = len(author_email)
+    except Exception as e:
+        continue
+
+for author, stats in authors.items():
+    email = stats["email"]
+    lines = stats["lines"]
+    commits = len(stats["commits"])
+    files = len(stats["files"])
+    print(
+        f"{author:{max_lenght_author}} {email:{max_lenght_email}} {lines:6} {commits:6} {files:6}"
+    )
diff --git a/tools/git-blame-stats/go.mod b/tools/git-blame-stats/go.mod
deleted file mode 100644
index 4738ac4..0000000
--- a/tools/git-blame-stats/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-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
deleted file mode 100644
index 8b1bc9a..0000000
--- a/tools/git-blame-stats/main.go
+++ /dev/null
@@ -1,86 +0,0 @@
-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
-}