Rationalize steps management
[git-chart] / git-chart
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use POSIX qw(ceil);
6 use Getopt::Long;
7 use Date::Parse;
8
9 my $SECS_PER_DAY = 24*3600;
10
11 my %steps = (
12         hourly => 3600,
13         daily => $SECS_PER_DAY,
14         weekly => 7*$SECS_PER_DAY,
15         monthly => 30*$SECS_PER_DAY,
16 );
17
18 sub usage() {
19         print "Usage: git chart\n";
20 }
21
22 sub gather_data($) {
23         my $options = shift;
24         my %dataset;
25
26         print "Gathering data ...\n";
27
28         my $groups = 0;
29         my $commits = 0;
30
31         my $step=$options->{step};
32
33         open my $fd, '-|', qw(git log --date=raw), '--pretty=%ad %s', @{$options->{cmdline}};
34         die "failed to get revs" unless $fd;
35         while (<$fd>) {
36                 chomp;
37                 $commits += 1;
38                 my ($date, $tz, $sub) = split ' ', $_, 3;
39                 # TODO use $tz
40
41                 my $key = $date - ($date % $step);
42                 if (exists $dataset{$key}) {
43                         $dataset{$key} += 1;
44                 } else {
45                         $dataset{$key} = 1;
46                         $groups += 1;
47                 }
48         }
49         close $fd;
50
51         print "...done, $commits commits in $groups groups.\n";
52
53         # fill missing steps and find max
54         my @keys = sort keys %dataset;
55         my $last = pop @keys;
56         my $max = 0;
57         while (my $key = shift @keys) {
58                 $max = $dataset{$key} if $max < $dataset{$key};
59                 my $next = $key + $step;
60                 while (! exists $dataset{$next}) {
61                         $dataset{$next} = 0;
62                         $next += $step;
63                         last if $next >= $last;
64                 }
65         }
66
67         $options->{max} = $max;
68         return \%dataset;
69 }
70
71 # functions to plot the datasets.
72 # each function can be called with either one or two parameters.
73 # when called with two parameters, the first is assumed to be the dataset, and the second the options
74 # (array and hash ref respectively).
75 # when called with a single parameter, it is assumed to be an options hash ref, and the dataset is 
76 # created by calling gather_data with the passed options.
77
78 # google chart API
79 # TODO needs a lot of customization
80 sub google_chart($;$) {
81         my $dataset = shift;
82         my $options = shift;
83         if (! defined $options) {
84                 $options = $dataset;
85                 $dataset = gather_data($options);
86         }
87
88         my $height=$options->{chart_height};
89         my $max = $options->{max};
90
91         my @keys = sort keys %$dataset;
92         my $from = $keys[0];
93         my $to = $keys[@keys-1];
94
95         my @data;
96         while (my $key = shift @keys) {
97                 push @data, $dataset->{$key};
98         }
99
100         my $width=ceil(4*$height/3);
101
102         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";
103
104         my $launch = sprintf $url, join(",",@data);
105         print $launch, "\n";
106         # `git web--browse "$launch"`
107 }
108
109 # gnuplot
110 sub gnuplot_chart($;$) {
111         my $dataset = shift;
112         my $options = shift;
113         if (! defined $options) {
114                 $options = $dataset;
115                 $dataset = gather_data($options);
116         }
117
118         my @keys = sort keys %$dataset;
119         my $step=$options->{step};
120         my $from = $keys[0];
121         my $to = $keys[@keys-1];
122         my $data = '';
123         while (my $key = shift @keys) {
124                 $data .= "$key $dataset->{$key}\n";
125         }
126         my $max = $options->{max};
127
128         # TODO allow customization
129         # in particular, detect (lack of) display and set term to dumb accordingly
130         my $termcmd = $options->{gnuplot_term};
131
132         my $plotsetup = $options->{gnuplot_setup};
133         $plotsetup .= "\nset yrange [0:$max]\n";
134         $plotsetup .= "set xrange ['$from':'$to']\n";
135         my ($formatx, $ticks);
136         if ($to - $from > $steps{monthly}) {
137                 $formatx = "%b\\n%Y";
138                 $ticks = $steps{monthly};
139         } elsif ($to - $from > 2*$steps{daily}) {
140                 $formatx = "%d\\n%b";
141                 $ticks = $steps{daily};
142         } else {
143                 $formatx = "%H:%M";
144                 $ticks = $steps{hourly};
145         }
146         $plotsetup .= "set format x \"$formatx\"\n";
147         $plotsetup .= "set xtics $ticks\n";
148         my $plotstyle = $options->{gnuplot_style};
149         my $plotoptions = $options->{gnuplot_plotwith};
150
151         open my $gp, "|gnuplot -persist";
152
153         my $gp_script = <<GPCMD
154 $termcmd
155 set xdata time
156 set timefmt "%s"
157 $plotsetup
158 $plotstyle
159 plot "-" using 1:2 $plotoptions
160 $data
161 GPCMD
162         ;
163
164         print STDOUT $gp_script;
165         print $gp $gp_script;
166         close $gp;
167 }
168
169 # some defaults
170 my %options = (
171         step => $SECS_PER_DAY,
172         cmdline => [],
173         # charting/plotting options
174         plotter => \&gnuplot_chart,
175         chart_height => 144,
176         gnuplot_term => '',
177         gnuplot_setup => "set nokey",
178         gnuplot_style => 'set style data histeps',
179         gnuplot_plotwith => '',
180 );
181
182 sub parse_step(@) {
183         my $key = shift;
184         my $step = shift;
185         if (exists $steps{$key}) {
186                 $options{step} = $steps{$key};
187                 return;
188         }
189         die "this can't happen ($key)" unless $key eq 'step';
190
191         if ($step =~/^\d+$/) {
192                 $options{step} = 0 + $step;
193                 return
194         } else {
195                 if (exists $steps{$step}) {
196                         $options{step} = $steps{$step};
197                         return;
198                 }
199                 die "unknown step $step";
200         }
201 }
202
203 # read our options first
204 Getopt::Long::Configure('pass_through');
205 GetOptions(
206         'hourly' => \&parse_step,
207         'daily' => \&parse_step,
208         'weekly' => \&parse_step,
209         'monthly' => \&parse_step,
210         'step=s' => sub { parse_step(@_) },
211         'chart-height=i' => sub { $options{chart_height} = $_[1]},
212         google => sub { $options{plotter} = \&google_chart },
213         gnuplot => sub { $options{plotter} = \&gnuplot_chart },
214 );
215
216 # if anything was left, check for log options
217 if (@ARGV) {
218         $options{cmdline} = \@ARGV;
219 }
220
221 die "step must be strictly positive!" unless $options{step} > 0;
222
223 $options{plotter}->(\%options);