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)
7 # * allow the steps to be counted from the most recent rather than from the
12 use POSIX qw(ceil strftime);
16 my $SECS_PER_DAY = 24*3600;
20 daily => $SECS_PER_DAY,
21 weekly => 7*$SECS_PER_DAY,
22 monthly => 30*$SECS_PER_DAY,
23 yearly => 12*30*$SECS_PER_DAY,
27 hourly => "%Y-%m-%d %H:00",
28 daily => "%Y-%m-%d 00:00",
29 monthly => "%Y-%m-01 00:00",
30 yearly => "%Y-01-01 00:00",
35 print "Usage: git chart\n";
41 print "Gathering data ...\n";
45 open my $fd, '-|', qw(git log --date=local), '--pretty=%ad,%s', @{$options->{cmdline}};
46 die "failed to get revs" unless $fd;
49 my ($date, $sub) = split ',', $_, 2;
51 push @commits, str2time($date);
56 @commits = sort @commits;
58 print "...done, " . @commits . " commits.\n";
60 my $gap = $commits[$#commits] - $commits[0];
63 if (defined $options->{step}) {
64 $step = $options->{step};
66 $step = $gap > $steps{yearly} ? 'monthly' :
67 $gap > $steps{monthly} ? 'weekly' :
68 $gap > $steps{weekly}/2 ? 'daily' :
71 $options->{step} = $step;
74 # truncate each commit (date) to the wanted step
75 # e.g. if the step is 'monthly', then commit YYYY/MM/DD
78 for my $commit (@commits) {
80 if (exists $trunc{$step}) {
81 # LOL -- no, seriously, is there a smarter way to do this?
82 $key = str2time(strftime($trunc{$step}, localtime($commit)));
84 $key = $commit - ($commit % $step);
86 if (exists $dataset{$key}) {
93 # fill missing steps and find max
94 my @keys = sort keys %dataset;
96 # ensure numeric step, taking into account
97 # that our numerical steps are approximate
99 if (exists $steps{$step}) {
100 $step = $steps{$step};
101 $tolerance = $step/2;
105 my $key = shift @keys;
107 $max = $dataset{$key} if $max < $dataset{$key};
108 my $next_step = $key + $step;
109 my $next = shift @keys;
111 while (abs($next - $next_step) > $tolerance) {
112 # next step is too far away
113 $dataset{$next_step} = 0;
119 $options->{max} = $max;
123 # functions to plot the datasets.
124 # each function can be called with either one or two parameters.
125 # when called with two parameters, the first is assumed to be the dataset, and the second the options
126 # (array and hash ref respectively).
127 # when called with a single parameter, it is assumed to be an options hash ref, and the dataset is
128 # created by calling gather_data with the passed options.
131 # TODO needs a lot of customization
132 sub google_chart($;$) {
135 if (! defined $options) {
137 $dataset = gather_data($options);
140 my $height=$options->{chart_height};
141 my $max = $options->{max};
143 my @keys = sort keys %$dataset;
145 my $to = $keys[@keys-1];
148 while (my $key = shift @keys) {
149 push @data, $dataset->{$key};
152 my $width=ceil(4*$height/3);
154 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";
156 my $launch = sprintf $url, join(",",@data);
158 # `git web--browse "$launch"`
162 sub gnuplot_chart($;$) {
165 if (! defined $options) {
167 $dataset = gather_data($options);
170 my @keys = sort keys %$dataset;
171 my $step=$options->{step};
173 my $to = $keys[$#keys];
176 while (my $key = shift @keys) {
177 $data .= "$key $dataset->{$key}\n";
179 my $max = $options->{max};
181 # add a fake datapoint lest to prevent the last
182 # datum from becoming invibile with style data steps
183 if (exists $steps{$step}) {
184 $to += $steps{$step};
190 # TODO allow customization
191 # in particular, detect (lack of) display and set term to dumb accordingly
192 my $termcmd = $options->{gnuplot_term};
194 my $plotsetup = $options->{gnuplot_setup};
195 $plotsetup .= "\nset yrange [0:$max]\n";
196 $plotsetup .= "set xrange ['$from':'$to']\n";
197 my ($formatx, $ticks);
198 if ($to - $from > $steps{yearly}) {
200 $ticks = $steps{yearly};
201 } elsif ($to - $from > $steps{monthly}) {
202 $formatx = "%b\\n%Y";
203 $ticks = $steps{monthly};
204 } elsif ($to - $from > 2*$steps{daily}) {
205 $formatx = "%d\\n%b";
206 $ticks = $steps{daily};
208 $formatx = "%H\\n%d";
209 $ticks = $steps{hourly};
211 $plotsetup .= "set format x \"$formatx\"\n";
212 $plotsetup .= "set xtics $ticks\n";
213 my $plotstyle = $options->{gnuplot_style};
214 my $plotoptions = $options->{gnuplot_plotwith};
216 open my $gp, "|gnuplot -persist";
218 my $gp_script = <<GPCMD
224 plot "-" using 1:2 $plotoptions
229 print STDOUT $gp_script;
230 print $gp $gp_script;
237 # charting/plotting options
238 plotter => \&gnuplot_chart,
241 gnuplot_setup => "set nokey",
242 gnuplot_style => 'set style data steps',
243 gnuplot_plotwith => '',
249 if (exists $steps{$key}) {
250 $options{step} = $key;
253 die "this can't happen ($key)" unless $key eq 'step';
255 if ($step =~/^\d+$/) {
256 $options{step} = 0 + $step;
259 if (exists $steps{$step}) {
260 $options{step} = $step;
263 die "unknown step $step";
267 # read our options first
268 Getopt::Long::Configure('pass_through');
270 'hourly' => \&parse_step,
271 'daily' => \&parse_step,
272 'weekly' => \&parse_step,
273 'monthly' => \&parse_step,
274 'yearly' => \&parse_step,
275 'step=s' => sub { parse_step(@_) },
276 'chart-height=i' => sub { $options{chart_height} = $_[1]},
277 google => sub { $options{plotter} = \&google_chart },
278 gnuplot => sub { $options{plotter} = \&gnuplot_chart },
281 # if anything was left, check for log options
283 $options{cmdline} = \@ARGV;
286 die "step must be strictly positive!" if defined $options{step} and not $options{step} > 0;
288 $options{plotter}->(\%options);