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