#!/usr/bin/perl use strict; use warnings; use POSIX qw(ceil); use Getopt::Long; use Date::Parse; sub usage() { 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}*24*3600; # days to seconds 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 $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 $plotstyle = $options->{gnuplot_style}; my $plotoptions = $options->{gnuplot_plotwith}; open my $gp, "|gnuplot -persist"; my $gp_script = < 1, cmdline => [], # charting/plotting options plotter => \&gnuplot_chart, chart_height => 144, gnuplot_term => '', gnuplot_setup => "set nokey", gnuplot_style => 'set style data histeps', gnuplot_plotwith => '', ); my %steps = ( daily => 1, weekly => 7, monthly => 30, ); 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( 'daily' => \&parse_step, 'weekly' => \&parse_step, 'monthly' => \&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);