#!/usr/bin/perl use strict; use utf8; use Getopt::Long; use Pod::Usage; use Data::Dumper; binmode(STDOUT, ":utf8"); #command line options my ( $help, $period, $author, $filepath ); GetOptions( 'help|?' => \$help, 'period|p=n' => \$period, 'author=s' => \$author, ) 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 @colors = ( 237, 157, 155, 47, 2 ); my @months = qw (Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); 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 ); my ( $total_commits, $max_streak, $cur_streak, $max_streak_weekdays, $cur_streak_weekdays ); my ( $cur_start, $max_start, $max_end, $cur_weekdays_start, $max_weekdays_start, $max_weekdays_end ); #loads of global variables sub process { #try to exit gracefully when the terminal doesn't support enough colors no warnings; #dont warn if tput command fails my $colors_supported = qx/tput colors/; if ($colors_supported && $colors_supported < 256) { chomp $colors_supported; print "fatal: 'tput colors' returned < 256 (" . $colors_supported . ") , cannot plot the calendar as the terminal doesn't support enough colors\n"; #will try to hack around this soon exit(1); } init_cal_stuff(); my $extra_args = ""; $extra_args = " --author=" . $author if $author; if ($filepath) { if ( -e $filepath ) { $extra_args .= " -- " . $filepath; } else { print "fatal: $filepath do not exists\n"; exit(2); } } my $git_command = "git log --pretty=format:\"%at\" --since=\"13 months\"" . $extra_args; #commits might not be in strict time order, check some past too my $epochs = qx/$git_command/; if ($?) { print "fatal: git-cal failed to get the git log\n"; exit(2); } my @epochs = split /\n/, $epochs; if (! @epochs) { print "git-cal: got empty log, nothing to do\n"; exit(1); } my $status; foreach (@epochs) { $status = add_epoch($_); last if !$status; } compute_stats(); print_grid(); } 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 -1 and 1 to 12" if ( defined $period && ( $period < -11 || $period > 12 || $period == 0 ) ); $period = 0 if !defined $period; if ( $period == 0 ) { $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 ); ( $total_commits, $max_streak, $cur_streak, $max_streak_weekdays, $cur_streak_weekdays ) = (0) x 5; ( $cur_start, $max_start, $max_end, $cur_weekdays_start, $max_weekdays_start, $max_weekdays_end ) = (0) x 6; } sub add_epoch { my $epoch = shift; 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 ); return 1; } sub add_to_grid { my ( $pos, $epoch ) = @_; my $r = int $pos / 7; my $c = $pos % 7; $grid[$r][$c]->{commits}++; $grid[$r][$c]->{epoch} = $epoch; $max_commits = $grid[$r][$c]->{commits} if $grid[$r][$c]->{commits} > $max_commits; } sub compute_stats { my %commit_counts; 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_start == 0; 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_weekdays_start == 0; 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"; } sub print_grid { my $space = 6; print_month_names($space); foreach my $c ( 0 .. 6 ) { printf "\n%" . ( $space - 2 ) . "s", ""; if ( $c == 1 ) { print "M "; } elsif ( $c == 3 ) { print "W "; } elsif ( $c == 5 ) { print "F "; } else { print " "; } 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 = 0; #$index = ( int( ( $val - 4 ) / $divide ) ) + 1 if $val > 0; #too dumb and bad if ($val) { if ( $val <= $q1 ) { $index = 1; } elsif ( $val <= $q2 ) { $index = 2; } elsif ( $val <= $q3 ) { $index = 3; } else { $index = 4; } } print_block($index); } else { print " "; } } } print "\n\n"; printf "%" . ( 2 * ( $row_end - $row_start ) + $space - 10 ) . "s", "Less "; #such that the right borders align print_block($_) foreach ( 0 .. 4 ); print " More\n"; printf "%4d: Total commits\n", $total_commits; print_message( $max_streak_weekdays, $max_weekdays_start, $max_weekdays_end, "Longest streak excluding weekends" ); print_message( $max_streak, $max_start, $max_end, "Longest streak including weekends" ); print_message( $cur_streak_weekdays, $cur_weekdays_start, time, "Current streak" ); } sub print_block { my $index = shift; $index = 4 if $index > 4; my $c = $colors[$index]; #always show on a black background, else it looks different (sometimes really bad ) with different settings. #print "\e[40;38;5;${c}m⬛ \e[0m"; print "\e[40;38;5;${c}m\x{25fc} \e[0m"; } 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 = 11 + ( $period || -11 ); 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; } __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 git-cal git-cal --author= -- =head2 OPTIONS --author view commits of a particular author (passed to git log --author= ) --period|p Do not show the entire year, p=1 to 12 shows only one month (1 = Jan .. 12 = Dec), p=-1 to -11 shows last p months and the current month --help|? help me =head2 ADDITIONAL OPTIONS -- filename to view the logs of a particular file or directory =head1 AUTHOR Karthik katooru =head1 COPYRIGHT AND LICENSE This program is free software; you can redistribute it and/or modify it under the MIT License