516 lines
14 KiB
Perl
Executable file
516 lines
14 KiB
Perl
Executable file
#!/usr/bin/perl
|
|
#
|
|
# Github like contributions calendar on terminal.
|
|
#
|
|
# Source: https://github.com/k4rthik/git-cal
|
|
#
|
|
use strict;
|
|
use warnings;
|
|
|
|
use utf8;
|
|
use Getopt::Long;
|
|
use Pod::Usage;
|
|
use Time::Local;
|
|
use Data::Dumper;
|
|
|
|
binmode(STDOUT, ":utf8");
|
|
#command line options
|
|
my ( $help, $period, $use_ascii, $use_ansi, $use_unicode, $format, $author, $filepath, $all_branches );
|
|
|
|
GetOptions(
|
|
'help|?' => \$help,
|
|
'period|p=n' => \$period,
|
|
'ascii' => \$use_ascii,
|
|
'ansi' => \$use_ansi,
|
|
'unicode' => \$use_unicode,
|
|
'author=s' => \$author,
|
|
'all' => \$all_branches
|
|
) or pod2usage(2);
|
|
|
|
pod2usage(1) if $help;
|
|
|
|
$filepath = shift @ARGV;
|
|
|
|
# also tried to use unicode chars instead of colors, the exp did not go well
|
|
#qw(⬚ ⬜ ▤ ▣ ⬛)
|
|
#qw(⬚ ▢ ▤ ▣ ⬛)
|
|
|
|
my @unicode = qw(⬚ ▢ ▤ ▣ ⬛);
|
|
# my @colors = ( 237, 139, 40, 190, 1 ); original
|
|
my @colors = ( 235, 58, 64, 70, 82 );
|
|
my @ascii = ( " ", ".", "o", "O", "0" );
|
|
my @months = qw (Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
|
|
|
|
configure(\$period, \$use_ascii, \$use_ansi, \$use_unicode, \$author);
|
|
|
|
process();
|
|
|
|
# 53 X 7 grid
|
|
# consists of 0 - 370 blocks
|
|
my ( @grid, @timeline, %pos_month, %month_pos, $jan1, $cur_year, $max_epoch, $min_epoch, $max_commits, $q1, $q2, $q3 );
|
|
my ( $first_block, $last_block, $start_block, $end_block, $row_start, $row_end );
|
|
#loads of global variables
|
|
|
|
sub process {
|
|
$format
|
|
= $use_ansi ? 'ansi'
|
|
: $use_ascii ? 'ascii'
|
|
: $use_unicode ? 'unicode'
|
|
: undef;
|
|
|
|
#if the user decided not to choose the format, let's pick some environmentally smart format
|
|
if ( !defined $format ) {
|
|
$format
|
|
= $ENV{EMACS} ? 'unicode'
|
|
: $ENV{TERM} eq 'dumb' ? 'ascii'
|
|
: 'ansi';
|
|
}
|
|
init_cal_stuff();
|
|
process_current_repo();
|
|
my %stats = compute_stats();
|
|
print_grid(%stats);
|
|
}
|
|
|
|
sub process_current_repo {
|
|
my $git_command = git_command();
|
|
my @epochs = qx/$git_command/;
|
|
|
|
if ($?) {
|
|
print "fatal: git-cal failed to get the git log\n";
|
|
exit(2);
|
|
}
|
|
|
|
if ( !@epochs ) {
|
|
print "git-cal: got empty log, nothing to do\n";
|
|
exit(1);
|
|
}
|
|
my $status;
|
|
foreach (@epochs) {
|
|
$status = add_epoch($_);
|
|
last if !$status;
|
|
}
|
|
}
|
|
|
|
sub git_command {
|
|
my $command = qq{git log --no-merges --pretty=format:"%at" --since="13 months"};
|
|
$command .= qq{ --author="$author"} if $author;
|
|
$command .= qq{ --all } if $all_branches;
|
|
if ($filepath) {
|
|
if ( -e $filepath ) {
|
|
$command .= qq{ -- $filepath};
|
|
}
|
|
else {
|
|
print "fatal: $filepath: no such file or directory\n";
|
|
exit(2);
|
|
}
|
|
}
|
|
return $command;
|
|
}
|
|
|
|
sub init_cal_stuff {
|
|
my ( $wday, $yday, $month, $year ) = ( localtime(time) )[ 6, 7, 4, 5 ];
|
|
$cur_year = $year;
|
|
$jan1 = 370 - ( $yday + 6 - $wday );
|
|
$last_block = $jan1 + $yday + 1;
|
|
$first_block = $last_block - 365;
|
|
$max_commits = 0;
|
|
push @timeline, $jan1;
|
|
$month_pos{0} = $jan1;
|
|
my $cur = $jan1;
|
|
|
|
foreach ( 0 .. $month - 1 ) {
|
|
$cur += number_of_days( $_, $year );
|
|
push @timeline, $cur;
|
|
$month_pos{ $_ + 1 } = $cur;
|
|
}
|
|
$cur = $jan1;
|
|
for ( my $m = 11; $m > $month; $m-- ) {
|
|
$cur -= number_of_days( $m, $year - 1 );
|
|
unshift @timeline, $cur;
|
|
$month_pos{$m} = $cur;
|
|
}
|
|
|
|
$pos_month{ $month_pos{$_} } = $months[$_] foreach keys %month_pos;
|
|
|
|
die "period can only be between -11 to 0 and 1 to 12" if ( defined $period && ( $period < -11 || $period > 12 ) );
|
|
if ( !defined $period ) {
|
|
$start_block = $first_block;
|
|
$end_block = $last_block;
|
|
}
|
|
elsif ( $period > 0 ) {
|
|
$start_block = $month_pos{ $period - 1 };
|
|
$end_block = $month_pos{ $period % 12 };
|
|
$end_block = $last_block if $start_block > $end_block;
|
|
}
|
|
else {
|
|
$start_block = $timeline[ 11 + $period ];
|
|
$start_block = $first_block if $period == -12;
|
|
$end_block = $last_block;
|
|
}
|
|
$row_start = int $start_block / 7;
|
|
$row_end = int $end_block / 7;
|
|
$max_epoch = time - 86400 * ( $last_block - $end_block );
|
|
$min_epoch = time - 86400 * ( $last_block - $start_block );
|
|
|
|
}
|
|
|
|
|
|
sub add_epoch {
|
|
my ($epoch, $count) = @_;
|
|
if ( $epoch > $max_epoch || $epoch < $min_epoch ) {
|
|
return 1;
|
|
}
|
|
my ( $month, $year, $wday, $yday ) = ( localtime($epoch) )[ 4, 5, 6, 7 ];
|
|
my $pos;
|
|
if ( $year == $cur_year ) {
|
|
$pos = ( $jan1 + $yday );
|
|
}
|
|
else {
|
|
my $total = ( $year % 4 ) ? 365 : 366;
|
|
$pos = ( $jan1 - ( $total - $yday ) );
|
|
}
|
|
return 0 if $pos < 0; #just in case
|
|
add_to_grid( $pos, $epoch, $count );
|
|
return 1;
|
|
}
|
|
|
|
sub add_to_grid {
|
|
my ( $pos, $epoch, $count ) = @_;
|
|
$count ||= 1;
|
|
my $r = int $pos / 7;
|
|
my $c = $pos % 7;
|
|
$grid[$r][$c]->{commits}+=$count;
|
|
$grid[$r][$c]->{epoch} = $epoch;
|
|
$max_commits = $grid[$r][$c]->{commits} if $grid[$r][$c]->{commits} > $max_commits;
|
|
}
|
|
|
|
|
|
sub compute_stats {
|
|
my %commit_counts;
|
|
|
|
my (
|
|
$total_commits,
|
|
$cur_streak,
|
|
$cur_start,
|
|
$max_streak,
|
|
$max_start,
|
|
$max_end,
|
|
$cur_streak_weekdays,
|
|
$cur_weekdays_start,
|
|
$max_streak_weekdays,
|
|
$max_weekdays_start,
|
|
$max_weekdays_end,
|
|
$q1,
|
|
$q2,
|
|
$q3,
|
|
) = (0) x 14;
|
|
|
|
foreach my $r ( $row_start .. $row_end ) {
|
|
foreach my $c ( 0 .. 6 ) {
|
|
my $cur_block = ( $r * 7 ) + $c;
|
|
if ( $cur_block >= $start_block && $cur_block < $end_block ) {
|
|
my $count = $grid[$r][$c]->{commits} || 0;
|
|
$total_commits += $count;
|
|
if ($count) {
|
|
$commit_counts{$count} = 1;
|
|
$cur_streak++;
|
|
$cur_start ||= $grid[$r][$c]->{epoch};
|
|
if ( $cur_streak > $max_streak ) {
|
|
$max_streak = $cur_streak;
|
|
$max_start = $cur_start;
|
|
$max_end = $grid[$r][$c]->{epoch};
|
|
}
|
|
|
|
#count++ if you work on weekends and streak will not be broken otherwise :)
|
|
$cur_streak_weekdays++;
|
|
$cur_weekdays_start ||= $grid[$r][$c]->{epoch};
|
|
if ( $cur_streak_weekdays > $max_streak_weekdays ) {
|
|
$max_streak_weekdays = $cur_streak_weekdays;
|
|
$max_weekdays_start = $cur_weekdays_start;
|
|
$max_weekdays_end = $grid[$r][$c]->{epoch};
|
|
}
|
|
}
|
|
else {
|
|
$cur_streak = 0;
|
|
$cur_start = 0;
|
|
if ( $c > 0 && $c < 6 ) {
|
|
$cur_streak_weekdays = 0;
|
|
$cur_weekdays_start = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#now compute quartiles
|
|
my @commit_counts = sort { $a <=> $b } ( keys %commit_counts );
|
|
$q1 = $commit_counts[ int( scalar @commit_counts ) / 4 ];
|
|
$q2 = $commit_counts[ int( scalar @commit_counts ) / 2 ];
|
|
$q3 = $commit_counts[ int( 3 * ( scalar @commit_counts ) / 4 ) ];
|
|
|
|
#print "commit counts: " . (scalar @commit_counts) . " - " . (join ",",@commit_counts) . "\n\n";
|
|
#print "quartiles: $q1 $q2 $q3\n";
|
|
|
|
#die Dumper \%stat;
|
|
|
|
my %stat = (
|
|
total_commits => $total_commits,
|
|
cur_streak => $cur_streak ,
|
|
cur_start => $cur_start ,
|
|
max_streak => $max_streak ,
|
|
max_start => $max_start ,
|
|
max_end => $max_end ,
|
|
cur_streak_weekdays => $cur_streak_weekdays,
|
|
cur_weekdays_start => $cur_weekdays_start,
|
|
max_streak_weekdays => $max_streak_weekdays,
|
|
max_weekdays_start => $max_weekdays_start,
|
|
max_weekdays_end => $max_weekdays_end ,
|
|
q1 => $q1,
|
|
q2 => $q2,
|
|
q3 => $q3,
|
|
);
|
|
|
|
|
|
return %stat;
|
|
}
|
|
|
|
sub print_grid {
|
|
my %stat = @_;
|
|
|
|
my $space = 6;
|
|
print_month_names($space);
|
|
foreach my $c ( 0 .. 6 ) {
|
|
printf "\n%" . ( $space - 2 ) . "s", "";
|
|
|
|
print $c == 1 ? "M "
|
|
: $c == 3 ? "W "
|
|
: $c == 5 ? "F "
|
|
: " ";
|
|
|
|
foreach my $r ( $row_start .. $row_end ) {
|
|
my $cur_block = ( $r * 7 ) + $c;
|
|
if ( $cur_block >= $start_block && $cur_block < $end_block ) {
|
|
my $val = $grid[$r][$c]->{commits} || 0;
|
|
|
|
my $index = $val == 0 ? 0
|
|
: $val <= $stat{q1} ? 1
|
|
: $val <= $stat{q2} ? 2
|
|
: $val <= $stat{q3} ? 3
|
|
: 4;
|
|
|
|
print_block($index);
|
|
}
|
|
else {
|
|
print " ";
|
|
}
|
|
}
|
|
}
|
|
print "\n\n";
|
|
printf " %d total commits\t\t\tLess ", $stat{total_commits};
|
|
print_block($_) foreach ( 0 .. 4 );
|
|
printf " More\n\n";
|
|
}
|
|
|
|
|
|
sub print_block {
|
|
my $index = shift;
|
|
$index = 4 if $index > 4;
|
|
$_
|
|
= ( $format eq "ascii" ) ? "${ascii[$index]} "
|
|
: ( $format eq "unicode" ) ? "\e[38;5;$colors[$index]m${unicode[$index]} \e[0m"
|
|
: "\e[38;5;$colors[$index]m \e[0m";
|
|
print;
|
|
}
|
|
|
|
|
|
sub print_month_names {
|
|
#print month labels, printing current month in the right position is tricky
|
|
my $space = shift;
|
|
if ( defined $period && $period > 0 ) {
|
|
printf "%" . $space . "s %3s", "", $months[ $period - 1 ];
|
|
return;
|
|
}
|
|
my $label_printer = 0;
|
|
my $timeline_iter = defined $period ? 11 + $period : 0;
|
|
if ( $start_block == $first_block && $timeline[0] != 0 ) {
|
|
my $first_pos = int $timeline[0] / 7;
|
|
if ( $first_pos == 0 ) {
|
|
printf "%" . ( $space - 2 ) . "s", "";
|
|
print $pos_month{ $timeline[-1] } . " ";
|
|
print $pos_month{ $timeline[0] } . " ";
|
|
$timeline_iter++;
|
|
}
|
|
elsif ( $first_pos == 1 ) {
|
|
printf "%" . ( $space - 2 ) . "s", "";
|
|
print $pos_month{ $timeline[-1] } . " ";
|
|
}
|
|
else {
|
|
printf "%" . $space . "s", "";
|
|
printf "%-" . ( 2 * $first_pos ) . "s", $pos_month{ $timeline[-1] };
|
|
}
|
|
$label_printer = $first_pos;
|
|
}
|
|
else {
|
|
printf "%" . $space . "s", "";
|
|
$label_printer += ( int $start_block / 7 );
|
|
}
|
|
|
|
while ( $label_printer < $end_block / 7 && $timeline_iter <= $#timeline ) {
|
|
while ( ( int $timeline[$timeline_iter] / 7 ) != $label_printer ) { print " "; $label_printer++; }
|
|
print " " . $pos_month{ $timeline[$timeline_iter] } . " ";
|
|
$label_printer += 3;
|
|
$timeline_iter++;
|
|
}
|
|
}
|
|
|
|
sub print_message {
|
|
my ( $days, $start_epoch, $end_epoch, $message ) = @_;
|
|
if ($days) {
|
|
my @range;
|
|
foreach my $epoch ( $start_epoch, $end_epoch ) {
|
|
my ( $mday, $mon, $year ) = ( localtime($epoch) )[ 3, 4, 5 ];
|
|
my $s = sprintf( "%3s %2d %4d", $months[$mon], $mday, ( 1900 + $year ) );
|
|
push @range, $s;
|
|
}
|
|
printf "%4d: Days ( %-25s ) - %-40s\n", $days, ( join " - ", @range ), $message;
|
|
}
|
|
else {
|
|
printf "%4d: Days - %-40s\n", $days, $message;
|
|
}
|
|
}
|
|
|
|
sub number_of_days {
|
|
my ( $month, $year ) = @_;
|
|
return 30 if $month == 3 || $month == 5 || $month == 8 || $month == 10;
|
|
return 31 if $month != 1;
|
|
return 28 if $year % 4;
|
|
return 29;
|
|
}
|
|
|
|
sub configure {
|
|
my ($period, $ascii, $ansi, $unicode, $author) = @_;
|
|
my @wanted;
|
|
push @wanted, 'format' if (not grep { defined $$_ } ($ascii, $ansi, $unicode));
|
|
push @wanted, 'period' if (not defined $$period);
|
|
push @wanted, 'author' if (not defined $$author);
|
|
if (@wanted) {
|
|
my $git_command = "git config --get-regexp 'calendar\.*'";
|
|
my @parts = split(/\s/, qx/$git_command/);
|
|
if(@parts) {
|
|
my %config;
|
|
while(my ($key, $value) = splice(@parts, 0, 2)) {
|
|
$key =~ s/calendar\.//;
|
|
$config{$key} = $value;
|
|
}
|
|
local @ARGV = (map { ( "-$_" => $config{$_} ) }
|
|
grep { exists $config{$_} } @wanted);
|
|
GetOptions(
|
|
'period=n' => $period,
|
|
'format=s' => sub {
|
|
if ($_[1] eq 'ascii') { $$ascii ||= 1; }
|
|
elsif ($_[1] eq 'ansi') { $$ansi ||= 1; }
|
|
elsif ($_[1] eq 'unicode') { $$unicode ||= 1; }
|
|
},
|
|
'author=s' => $author
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
git-cal - A simple tool to view commits calendar (similar to github contributions calendar) on command line
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
"git-cal" is a tool to visualize the git commit history in github's contribution calendar style.
|
|
The calendar shows how frequently the commits are made over the past year or some choosen period
|
|
Activity can be displayed using ascii, ansi or unicode characters, default is choosen based on ENV
|
|
|
|
git-cal
|
|
|
|
git-cal --period=<1..12, -11..0>
|
|
|
|
git-cal --author=<author>
|
|
|
|
git-cal --ascii
|
|
|
|
git-cal --ansi
|
|
|
|
git-cal --unicode
|
|
|
|
git-cal <options> <filepath>
|
|
|
|
=head2 OPTIONS
|
|
|
|
=over
|
|
|
|
=item [--period|-p]=<n>
|
|
|
|
Do not show the entire year:
|
|
|
|
=over
|
|
|
|
=item n = 1 to 12
|
|
|
|
Shows only one month (1=Jan .. 12=Dec)
|
|
|
|
=item n = -11 to 0
|
|
|
|
Shows the previous -n months (and the current month)
|
|
|
|
=back
|
|
|
|
=item --author=<author>
|
|
|
|
View commits of a particular author.
|
|
|
|
=item --all
|
|
|
|
View stats from all branches.
|
|
|
|
=item --ascii
|
|
|
|
Display activity using ASCII characters instead of ANSI colors.
|
|
|
|
=item --ansi
|
|
|
|
Display activity using ANSI colors
|
|
|
|
=item --unicode
|
|
|
|
Display activity using unicode characters
|
|
|
|
=item --help|-?
|
|
|
|
Print this message.
|
|
|
|
=back
|
|
|
|
=head2 ADDITIONAL OPTIONS
|
|
|
|
<filepath> to view the logs of a particular file or directory
|
|
|
|
=head2 USING GIT CONFIG
|
|
|
|
git-cal uses the git config tool to store configuration on disk. Similar keys are used to
|
|
those listed above with the notable exception being the bundling of ascii, ansi and unicode
|
|
into a "format" key. Examples of the three supported keys are below.
|
|
|
|
git config --global calendar.format ascii
|
|
|
|
git config --global calendar.period 5
|
|
|
|
git config --global calendar.author karthik
|
|
|
|
A command line supplied option will override the matching option set using this method.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Karthik katooru <karthikkatooru@gmail.com>
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
This program is free software; you can redistribute it and/or modify it under the MIT License
|