In this tutorial, I will show you how to write a Git stats analysis CLI tool using Go. This tool will allow you to visualize your local Git contributions in a graph format, similar to the one shown on GitHub.com.

A few years ago, I built a desktop app using Electron, Meteor.js, and the gitlog package. This app scanned my local Git repositories and provided a nice contributions graph. However, I disliked using Electron due to its large app size compared to other options like MacGap.

To address this, I decided to port the app to Go and create a CLI command instead. This command will generate a graph of your Git contributions, similar to the one shown on GitHub.com.

You can find the code for this tutorial on this Gist: https://gist.github.com/flaviocopes/bf2f982ee8f2ae3f455b06c7b2b03695

Part 1: Acquire a list of folders to scan

To begin, we need to implement the first part of the program which involves acquiring a list of folders to scan for Git repositories. This can be done by creating a command-line interface that accepts the -add flag to add a new folder to the list, and without any flags, generates the graph.

Here is the skeleton code for this separation of concerns:

package main

import (
	"flag"
)

// scan given a path crawls it and its subfolders
// searching for Git repositories
func scan(path string) {
	print("scan")
}

// stats generates a nice graph of your Git contributions
func stats(email string) {
	print("stats")
}

func main() {
	var folder string
	var email string
	flag.StringVar(&folder, "add", "", "add a new folder to scan for Git repositories")
	flag.StringVar(&email, "email", "[email protected]", "the email to scan")
	flag.Parse()

	if folder != "" {
		scan(folder)
		return
	}

	stats(email)
}

Part 2: Generate the stats

The second part of the program involves generating the graph of Git contributions based on the list of folders acquired. For this, we will use the go-git dependency, which provides an easy-to-use API for interacting with Git repositories.

Here is the code for generating the stats:

// stats calculates and prints the stats.
func stats(email string) {
	commits := processRepositories(email)
	printCommitsStats(commits)
}

// processRepositories given a user email, returns the
// commits made in the last 6 months
func processRepositories(email string) map[int]int {
	filePath := getDotFilePath()
	repos := parseFileLinesToSlice(filePath)
	daysInMap := daysInLastSixMonths

	commits := make(map[int]int, daysInMap)
	for i := daysInMap; i > 0; i-- {
		commits[i] = 0
	}

	for _, path := range repos {
		commits = fillCommits(email, path, commits)
	}

	return commits
}

// fillCommits given a repository found in `path`, gets the commits and
// puts them in the `commits` map, returning it when completed
func fillCommits(email string, path string, commits map[int]int) map[int]int {
	repo, err := git.PlainOpen(path)
	if err != nil {
		panic(err)
	}

	ref, err := repo.Head()
	if err != nil {
		panic(err)
	}

	iterator, err := repo.Log(&git.LogOptions{From: ref.Hash()})
	if err != nil {
		panic(err)
	}

	offset := calcOffset()
	err = iterator.ForEach(func(c *object.Commit) error {
		daysAgo := countDaysSinceDate(c.Author.When) + offset

		if c.Author.Email != email {
			return nil
		}

		if daysAgo != outOfRange {
			commits[daysAgo]++
		}

		return nil
	})

	if err != nil {
		panic(err)
	}

	return commits
}

// printCommitsStats prints the commits stats
func printCommitsStats(commits map[int]int) {
	// Implementation omitted for brevity
}

To generate the stats, we iterate over each repository in the list and retrieve their commits using the go-git package. We then populate a commits map with the number of commits made on each day. Finally, we pass this map to the printCommitsStats function to print the graph.

With this implementation, you can now visualize your local Git contributions by running the CLI command with the email flag.

Tags: Git, Go, CLI, Git stats, Visualization