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