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-1];
173 while (my $key = shift @keys) {
174 $data .= "$key $dataset->{$key}\n";
176 my $max = $options->{max};
178 # TODO allow customization
179 # in particular, detect (lack of) display and set term to dumb accordingly
180 my $termcmd = $options->{gnuplot_term};
182 my $plotsetup = $options->{gnuplot_setup};
183 $plotsetup .= "\nset yrange [0:$max]\n";
184 $plotsetup .= "set xrange ['$from':'$to']\n";
185 my ($formatx, $ticks);
186 if ($to - $from > $steps{yearly}) {
188 $ticks = $steps{yearly};
189 } elsif ($to - $from > $steps{monthly}) {
190 $formatx = "%b\\n%Y";
191 $ticks = $steps{monthly};
192 } elsif ($to - $from > 2*$steps{daily}) {
193 $formatx = "%d\\n%b";
194 $ticks = $steps{daily};
197 $ticks = $steps{hourly};
199 $plotsetup .= "set format x \"$formatx\"\n";
200 $plotsetup .= "set xtics $ticks\n";
201 my $plotstyle = $options->{gnuplot_style};
202 my $plotoptions = $options->{gnuplot_plotwith};
204 open my $gp, "|gnuplot -persist";
206 my $gp_script = <<GPCMD
212 plot "-" using 1:2 $plotoptions
217 print STDOUT $gp_script;
218 print $gp $gp_script;
225 # charting/plotting options
226 plotter => \&gnuplot_chart,
229 gnuplot_setup => "set nokey",
230 gnuplot_style => 'set style data histeps',
231 gnuplot_plotwith => '',
237 if (exists $steps{$key}) {
238 $options{step} = $key;
241 die "this can't happen ($key)" unless $key eq 'step';
243 if ($step =~/^\d+$/) {
244 $options{step} = 0 + $step;
247 if (exists $steps{$step}) {
248 $options{step} = $step;
251 die "unknown step $step";
255 # read our options first
256 Getopt::Long::Configure('pass_through');
258 'hourly' => \&parse_step,
259 'daily' => \&parse_step,
260 'weekly' => \&parse_step,
261 'monthly' => \&parse_step,
262 'yearly' => \&parse_step,
263 'step=s' => sub { parse_step(@_) },
264 'chart-height=i' => sub { $options{chart_height} = $_[1]},
265 google => sub { $options{plotter} = \&google_chart },
266 gnuplot => sub { $options{plotter} = \&gnuplot_chart },
269 # if anything was left, check for log options
271 $options{cmdline} = \@ARGV;
274 die "step must be strictly positive!" if defined $options{step} and not $options{step} > 0;
276 $options{plotter}->(\%options);