#!/usr/bin/perl # List of general TODO # * autodetect optimal frequency depending on date range for commits # * default to something else than "the whole history", probably # * if no commits are specified and a commit grouping step is specified, # limit commits proportionally (e.g. --daily => one week worth of commits) # * find a better way to group commits, the current is a little too inaccurate # # Most of the mentioned TODOs require either some peeking at the commits # or some other form of pre/post-processing. For both it's probably better # to just slurp them at once and then think about the grouping use strict; use warnings; use POSIX qw(ceil); use Getopt::Long; use Date::Parse; my $SECS_PER_DAY = 24*3600; my %steps = ( hourly => 3600, daily => $SECS_PER_DAY, weekly => 7*$SECS_PER_DAY, monthly => 30*$SECS_PER_DAY, yearly => 12*30*$SECS_PER_DAY, ); sub usage() { # TODO print "Usage: git chart\n"; } sub gather_data($) { my $options = shift; my %dataset; print "Gathering data ...\n"; my $groups = 0; my $commits = 0; my $step=$options->{step}; open my $fd, '-|', qw(git log --date=raw), '--pretty=%ad %s', @{$options->{cmdline}}; die "failed to get revs" unless $fd; while (<$fd>) { chomp; $commits += 1; my ($date, $tz, $sub) = split ' ', $_, 3; # TODO use $tz my $key = $date - ($date % $step); if (exists $dataset{$key}) { $dataset{$key} += 1; } else { $dataset{$key} = 1; $groups += 1; } } close $fd; print "...done, $commits commits in $groups groups.\n"; # fill missing steps and find max my @keys = sort keys %dataset; my $last = pop @keys; my $max = 0; while (my $key = shift @keys) { $max = $dataset{$key} if $max < $dataset{$key}; my $next = $key + $step; while (! exists $dataset{$next}) { $dataset{$next} = 0; $next += $step; last if $next >= $last; } } $options->{max} = $max; return \%dataset; } # functions to plot the datasets. # each function can be called with either one or two parameters. # when called with two parameters, the first is assumed to be the dataset, and the second the options # (array and hash ref respectively). # when called with a single parameter, it is assumed to be an options hash ref, and the dataset is # created by calling gather_data with the passed options. # google chart API # TODO needs a lot of customization sub google_chart($;$) { my $dataset = shift; my $options = shift; if (! defined $options) { $options = $dataset; $dataset = gather_data($options); } my $height=$options->{chart_height}; my $max = $options->{max}; my @keys = sort keys %$dataset; my $from = $keys[0]; my $to = $keys[@keys-1]; my @data; while (my $key = shift @keys) { push @data, $dataset->{$key}; } my $width=ceil(4*$height/3); my $url="https://chart.googleapis.com/chart?chs=${width}x${height}&cht=bvg&chd=t:%s&chds=0,$max&chbh=a&chxt=y&chxr=0,0,$max"; my $launch = sprintf $url, join(",",@data); print $launch, "\n"; # `git web--browse "$launch"` } # gnuplot sub gnuplot_chart($;$) { my $dataset = shift; my $options = shift; if (! defined $options) { $options = $dataset; $dataset = gather_data($options); } my @keys = sort keys %$dataset; my $step=$options->{step}; my $from = $keys[0]; my $to = $keys[@keys-1]; my $data = ''; while (my $key = shift @keys) { $data .= "$key $dataset->{$key}\n"; } my $max = $options->{max}; # TODO allow customization # in particular, detect (lack of) display and set term to dumb accordingly my $termcmd = $options->{gnuplot_term}; my $plotsetup = $options->{gnuplot_setup}; $plotsetup .= "\nset yrange [0:$max]\n"; $plotsetup .= "set xrange ['$from':'$to']\n"; my ($formatx, $ticks); if ($to - $from > $steps{yearly}) { $formatx = "%Y"; $ticks = $steps{yearly}; } elsif ($to - $from > $steps{monthly}) { $formatx = "%b\\n%Y"; $ticks = $steps{monthly}; } elsif ($to - $from > 2*$steps{daily}) { $formatx = "%d\\n%b"; $ticks = $steps{daily}; } else { $formatx = "%H:%M"; $ticks = $steps{hourly}; } $plotsetup .= "set format x \"$formatx\"\n"; $plotsetup .= "set xtics $ticks\n"; my $plotstyle = $options->{gnuplot_style}; my $plotoptions = $options->{gnuplot_plotwith}; open my $gp, "|gnuplot -persist"; my $gp_script = < $SECS_PER_DAY, cmdline => [], # charting/plotting options plotter => \&gnuplot_chart, chart_height => 144, gnuplot_term => '', gnuplot_setup => "set nokey", gnuplot_style => 'set style data histeps', gnuplot_plotwith => '', ); sub parse_step(@) { my $key = shift; my $step = shift; if (exists $steps{$key}) { $options{step} = $steps{$key}; return; } die "this can't happen ($key)" unless $key eq 'step'; if ($step =~/^\d+$/) { $options{step} = 0 + $step; return } else { if (exists $steps{$step}) { $options{step} = $steps{$step}; return; } die "unknown step $step"; } } # read our options first Getopt::Long::Configure('pass_through'); GetOptions( 'hourly' => \&parse_step, 'daily' => \&parse_step, 'weekly' => \&parse_step, 'monthly' => \&parse_step, 'yearly' => \&parse_step, 'step=s' => sub { parse_step(@_) }, 'chart-height=i' => sub { $options{chart_height} = $_[1]}, google => sub { $options{plotter} = \&google_chart }, gnuplot => sub { $options{plotter} = \&gnuplot_chart }, ); # if anything was left, check for log options if (@ARGV) { $options{cmdline} = \@ARGV; } die "step must be strictly positive!" unless $options{step} > 0; $options{plotter}->(\%options);