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