#!/usr/bin/env perl

# This script plots the number of insertions and deletions
# in a Git repository over time.
# Example output: _▁▂▃▄▅▆▇█

# It depends on:
# - git, for retrieving the number of insertions/deletions
# - perl, because this script is written in Perl
# - tput, for retrieving the width of the terminal

# 2017-10-17: started with Git::Raw
# 2025-06-12: rewrote everything using `git`

use strict;
use warnings;
use open qw( :std :encoding(UTF-8) );    # spark lines are UTF-8

use IPC::Open2 qw(open2);

# # first statement in case this script is called just before midnight:
# my $now = time;

# my $cols = `tput cols 2>/dev/null` || 80;
# my $max_age;                             # leave undefined
# my $min_age;                             # leave undefined

# # normalize parameters (needed for range detection)
# my $args = join ' ', @ARGV;
# for my $arg (`git rev-parse $args`) {
#     if    ( $arg =~ /^--max-age=(\d+)$/ ) { $max_age = $1; }
#     elsif ( $arg =~ /^--min-age=(\d+)$/ ) { $min_age = $1; }
# }

# my $range = $cols * 86400;
# $max_age //= defined $min_age ? $min_age + $range : $now;
# $min_age //= defined $max_age ? $max_age - $range : $now - $range;
# warn "max: ", $max_age, "\n";
# warn "min: ", $min_age, "\n";
# die;

# by default plot as many days as terminal has columns
my $days = `tput cols` or die "\n";

# parse days from ARGV
while ( my ( $i, $arg ) = each @ARGV ) {
    if ( $arg =~ /^-(\d+)$/ ) {
        $days = $1;
        splice @ARGV, $i, 1;
        last;
    }
}

# calculate start time
use Time::Piece qw(localtime);
my $now   = localtime;
my $start = $now - $days * 86400;
my $since = sprintf "%04d-%02d-%02d", $start->year, $start->mon, $start->mday;

my $cmd =
"git log --no-merges --since='$since' --date=format:%Y-%m-%d --author-date-order --reverse --pretty=format:'%ad' --shortstat -z";

# don't override predefined arguments
my %predefined = map  { s/=.*//r => 1 } grep { /^-/ } split /\s+/, $cmd;
my @redefined  = grep { exists $predefined{s/=.*//r} } @ARGV;
warn "$_: predefined argument\n" for @redefined;
exit 1 if @redefined;

# quote user arguments to accomodate for spaces (e.g. `--author 'John Doe'`)
$cmd .= ' ' . join ' ', map { "'$_'" } @ARGV;
my $pid = open2( my $OUT, my $IN, $cmd ) or die "$cmd: $!\n";

my %changes;

local $/ = "\0";    # read zero-separated lines
while (<$OUT>) {

    # this is a hot loop; avoid unnecassary branches
    my ($ymd) = /^(\d\d\d\d-\d\d-\d\d)/;

    ( $changes{$ymd} ) += /, (\d+) ins/;
    ( $changes{$ymd} ) += /, (\d+) del/;
}

waitpid( $pid, 0 );
$? >> 8 and exit 1;

# convert entries to a lists without holes;
# calculate max
my @changes = ();
my $max     = 0;
for ( my $time = $start ; $time <= $now ; $time += 86400 ) {
    my $ymd     = $time->ymd;
    my $changes = $changes{$ymd} || 0;
    push @changes, $changes;
    $max = $changes > $max ? $changes : $max;
}

for ( my $i = 0 ; $i < $days ; $i++ ) {
    my $changes = $changes[$i];
    my $char = $changes ? 9601 + int( $changes * 8 / $max ) : 95;   # spark line
    printf "%c", $char;
}
print "\n";
