4 # * default to something else than "the whole history", probably
5 # * if no commits are specified and a commit grouping step is specified,
6 # limit commits proportionally (e.g. --daily => one week worth of commits)
10 use POSIX qw(ceil strftime);
14 my $SECS_PER_DAY = 24*3600;
18 daily => $SECS_PER_DAY,
19 weekly => 7*$SECS_PER_DAY,
20 monthly => 30*$SECS_PER_DAY,
21 yearly => 12*30*$SECS_PER_DAY,
25 hourly => "%Y-%m-%d %H:00",
26 daily => "%Y-%m-%d 00:00",
27 monthly => "%Y-%m-01 00:00",
28 yearly => "%Y-01-01 00:00",
33 print "Usage: git chart\n";
39 print "Gathering data ...\n";
43 open my $fd, '-|', qw(git log --date=local), '--pretty=%ad,%s', @{$options->{cmdline}};
44 die "failed to get revs" unless $fd;
47 my ($date, $sub) = split ',', $_, 2;
49 push @commits, str2time($date);
54 @commits = sort @commits;
56 print "...done, " . @commits . " commits.\n";
58 my $gap = $commits[$#commits] - $commits[0];
61 if (defined $options->{step}) {
62 $step = $options->{step};
64 $step = $gap > $steps{yearly} ? 'monthly' :
65 $gap > $steps{monthly} ? 'weekly' :
66 $gap > $steps{weekly}/2 ? 'daily' :
69 $options->{step} = $step;
72 # truncate each commit (date) to the wanted step
73 # e.g. if the step is 'monthly', then commit YYYY/MM/DD
76 for my $commit (@commits) {
78 if (exists $trunc{$step}) {
79 # LOL -- no, seriously, is there a smarter way to do this?
80 $key = str2time(strftime($trunc{$step}, localtime($commit)));
82 $key = $commit - ($commit % $step);
84 if (exists $dataset{$key}) {
91 # fill missing steps and find max
92 my @keys = sort keys %dataset;
94 # ensure numeric step, taking into account
95 # that our numerical steps are approximate
97 if (exists $steps{$step}) {
98 $step = $steps{$step};
103 my $key = shift @keys;
105 $max = $dataset{$key} if $max < $dataset{$key};
106 my $next_step = $key + $step;
107 my $next = shift @keys;
109 while (abs($next - $next_step) > $tolerance) {
110 # next step is too far away
111 $dataset{$next_step} = 0;
117 $options->{max} = $max;
121 # functions to plot the datasets.
122 # each function can be called with either one or two parameters.
123 # when called with two parameters, the first is assumed to be the dataset, and the second the options
124 # (array and hash ref respectively).
125 # when called with a single parameter, it is assumed to be an options hash ref, and the dataset is
126 # created by calling gather_data with the passed options.
129 # TODO needs a lot of customization
130 sub google_chart($;$) {
133 if (! defined $options) {
135 $dataset = gather_data($options);
138 my $height=$options->{chart_height};
139 my $max = $options->{max};
141 my @keys = sort keys %$dataset;
143 my $to = $keys[@keys-1];
146 while (my $key = shift @keys) {
147 push @data, $dataset->{$key};
150 my $width=ceil(4*$height/3);
152 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";
154 my $launch = sprintf $url, join(",",@data);
156 # `git web--browse "$launch"`
160 sub gnuplot_chart($;$) {
163 if (! defined $options) {
165 $dataset = gather_data($options);
168 my @keys = sort keys %$dataset;
169 my $step=$options->{step};
171 my $to = $keys[$#keys];
174 while (my $key = shift @keys) {
175 $data .= "$key $dataset->{$key}\n";
177 my $max = $options->{max};
179 # add a fake datapoint lest to prevent the last
180 # datum from becoming invibile with style data steps
181 if (exists $steps{$step}) {
182 $to += $steps{$step};
188 # TODO allow customization
189 # in particular, detect (lack of) display and set term to dumb accordingly
190 my $termcmd = $options->{gnuplot_term};
192 my $plotsetup = $options->{gnuplot_setup};
193 $plotsetup .= "\nset yrange [0:$max]\n";
194 $plotsetup .= "set xrange ['$from':'$to']\n";
195 my ($formatx, $ticks);
196 if ($to - $from > $steps{yearly}) {
198 $ticks = $steps{yearly};
199 } elsif ($to - $from > $steps{monthly}) {
200 $formatx = "%b\\n%Y";
201 $ticks = $steps{monthly};
202 } elsif ($to - $from > 2*$steps{daily}) {
203 $formatx = "%d\\n%b";
204 $ticks = $steps{daily};
206 $formatx = "%H\\n%d";
207 $ticks = $steps{hourly};
209 $plotsetup .= "set format x \"$formatx\"\n";
210 $plotsetup .= "set xtics $ticks\n";
211 my $plotstyle = $options->{gnuplot_style};
212 my $plotoptions = $options->{gnuplot_plotwith};
214 open my $gp, "|gnuplot -persist";
216 my $gp_script = <<GPCMD
222 plot "-" using 1:2 $plotoptions
227 print STDOUT $gp_script;
228 print $gp $gp_script;
235 # charting/plotting options
236 plotter => \&gnuplot_chart,
239 gnuplot_setup => "set nokey",
240 gnuplot_style => 'set style data steps',
241 gnuplot_plotwith => '',
247 if (exists $steps{$key}) {
248 $options{step} = $key;
251 die "this can't happen ($key)" unless $key eq 'step';
253 if ($step =~/^\d+$/) {
254 $options{step} = 0 + $step;
257 if (exists $steps{$step}) {
258 $options{step} = $step;
261 die "unknown step $step";
265 # read our options first
266 Getopt::Long::Configure('pass_through');
268 'hourly' => \&parse_step,
269 'daily' => \&parse_step,
270 'weekly' => \&parse_step,
271 'monthly' => \&parse_step,
272 'yearly' => \&parse_step,
273 'step=s' => sub { parse_step(@_) },
274 'chart-height=i' => sub { $options{chart_height} = $_[1]},
275 google => sub { $options{plotter} = \&google_chart },
276 gnuplot => sub { $options{plotter} = \&gnuplot_chart },
279 # if anything was left, check for log options
281 $options{cmdline} = \@ARGV;
284 die "step must be strictly positive!" if defined $options{step} and not $options{step} > 0;
286 $options{plotter}->(\%options);