Skip to content

adot-7/gitcrawl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gitcrawl

GitHub-style contribution heatmap in your terminal. Scans local Git repos, reads commit history, prints a week/day grid with month labels.

Built by following this tutorial. It has outdated libraries and subtle bugs. This documents what broke, how I approached the fixes, and a few things I added.


What it does

  • Recursively scans folders for Git repos
  • Persists discovered repos to a dotfile
  • Reads commit history for the last ~6 months
  • Buckets commits into week/day cells
  • Prints a terminal heatmap with month/day labels
  • Exposes debug flags for inspecting raw bucket state

Usage

# Add a folder to scan
go run ./gitcrawl -add ~/code

# Filter by your email
go run ./gitcrawl -email me@example.com

# Show all authors
go run ./gitcrawl -email "*@*.*"

# Debug commit bucketing
go run ./gitcrawl -debug commitsDump

# Debug author counts
go run ./gitcrawl -debug authorsMap

# Clear saved repos
go run ./gitcrawl -clear y

Requirements

go version

The project expects a working Go toolchain and a local machine with Git repositories already present on disk. It scans folders you add, so it is not meant for remote GitHub URLs.

If you want to run it with your own repos, the basic flow is:

go run ./gitcrawl -add ~/projects
go run ./gitcrawl -email me@example.com

That keeps the setup simple: add local repos once, then query the graph by email.


Where the repo list is stored

The scanned repository paths are stored in a dotfile in your home directory:

~/.gitlocalstats

That file is what -add and -clear operate on. It lives outside the project folder so the repo list can survive between runs and between different terminal sessions.


Example Output

The program prints a terminal heatmap, so the output is mostly visual. A typical run looks like this:

alt text

The exact values depend on your local repositories and the email you pass in.

If you want to preview the output from a terminal-friendly image tool later, you can also pipe or convert it outside the program, but the core app prints directly to stdout.


Limitations

  • It only scans local folders that you add.
  • It depends on the paths still existing on your machine.
  • Output alignment depends on terminal font and width.
  • Large or unusual repository layouts can still expose edge cases.
  • The graph currently focuses on terminal rendering rather than exporting to image files.

The main goal was not to build a polished product UI, but to understand the data flow and the date bucketing well enough to make the output correct.


What I changed from the tutorial

1. CLI: added -debug and -clear flags

The article had two flags: -add and -email.

-debug commitsDump and -debug authorsMap let you inspect raw state without touching print statements. -clear y resets the dotfile without opening it manually. Both saved a lot of iteration time.

2. Scanner: returned data instead of writing inside scan()

The article's scan() did everything internally — recursive walk, dotfile write, all in one function. Hard to test, hard to compose with new flags.

Split it: scan() returns []string, and main decides when to persist. So -clear and -add can both work after breaking up the function.

Also added explicit handling for permission denied errors and skipped vendor/ and node_modules/ directories.

3. go-git: upgraded from v4 to v6

The tutorial imported gopkg.in/src-d/go-git.v4, which is abandoned. Current module path is github.com/go-git/go-git/v6. API surface is mostly the same but I had to read the updated docs to find what moved.

4. Error handling: skip malformed index files instead of panicking

The article panicked on any repo.Log() error. Real repo lists include broken clones. idxfile.ErrMalformedIdxFile from go-git/v6/plumbing/format/idxfile lets you detect and skip those without killing the whole run.

5. Fixed out-of-range + offset bug

The article pattern:

daysAgo := countDaysSinceDate(c.Author.When) + offset
if daysAgo != outOfRange {
    commits[daysAgo]++
}

Problem: countDaysSinceDate returns sentinel outOfRange for commits outside the window. Adding offset to the sentinel before checking it changes the value, so the guard never fires. You get an index out of range panic.

Fix: check the sentinel before adding the offset, then add an explicit upper-bound guard after:

daysSince := countDaysSince(c.Author.When)
if daysSince != outOfRange {
    howManyDaysAgo := daysSince + offset
    if howManyDaysAgo <= daysInLastSixMonths {
        commits[howManyDaysAgo]++
    }
}

6. Author filtering: added wildcard mode

The article filtered commits by exact email match. Added *@*.* as a wildcard to include all authors. Also track per-author commit counts in a map, which surfaces via -debug authorsMap when something looks off.

7. Debug data dumps

switch debug {
case "authorsMap":
    fmt.Printf("authors not matching filter: %d\n", len(authors)-1)
    fmt.Printf("%v\n", authors)
case "commitsDump":
    fmt.Printf("%v\n", commits)
}

The heatmap output alone is not enough to validate bucketing. When the graph looked wrong, the raw commits[] array had to be inspected before it hit the rendering logic.

8. Cell/column bucketing

This was the trickiest part. The article's bucketing didn't align with rendering, so today's cell highlighted on the wrong day and month headers were off.

The article used the commit key directly as week and day-of-week:

week := int(k / 7)
dayinweek := k % 7
if dayinweek == 0 { col = column{} }
col = append(col, commits[k])
if dayinweek == 6 { cols[week] = col }

The rendering loop then printed rows 7 down to 1. The index math didn't match. I printed raw bucket contents during render to see what was happening:

for week, colm := range cols {
    fmt.Printf("Week: %d\tvalues: %v\n", week, colm)
}

That confirmed the mismatch. The bucketing was then rebuilt to match the render order:

dayOfWeek := k % 7   // 1:saturday, 2:friday ... 0:sunday
if dayOfWeek == 1 { col = column{} }
col = append(col, commits[k])
if dayOfWeek == 0 {
    cols[week_counter] = col
    week_counter++
}

Now when rendering does colValue[7-row], the indices are actually correct.

9. Rendering: day labels, month loop, today highlight

Small fixes that followed from the bucketing fix:

  • Today highlight: col == 1 && row == 8-offset instead of i == 0 && j == calcOffset()-1
  • Month advancement: initDate.AddDate(0, 0, 7) instead of week.Add(7 * time.Hour * 24) — the latter doesn't account for DST
  • Day labels on Mon/Wed/Fri only to avoid crowding

What the article got right

It was incomplete in the right ways. The library was outdated but the data structures were sound, so fixes were local. The bugs were real but not impossible. The week/day math wasn't explained, which forced me to think through the rendering logic and actually understand it instead of just running the code.

About

Local git contribution graph CLI

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages