diff options
Diffstat (limited to 'tools/git-blame-stats')
-rw-r--r-- | tools/git-blame-stats/default.nix | 24 | ||||
-rwxr-xr-x | tools/git-blame-stats/git-blame-stats.py | 95 | ||||
-rw-r--r-- | tools/git-blame-stats/go.mod | 3 | ||||
-rw-r--r-- | tools/git-blame-stats/main.go | 86 |
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 -} |