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",
36 Usage: git chart [options...] [log specs...]
38 Creates a chart plotting the distribution over time of the commits
39 specified as <log specs>. By default, commits are grouped by hour, day,
40 week, month or year depending on the time spanned, but this can be
41 controlled by the user.
44 --hourly, --daily, --weekly, --monthly, --yearly:
45 force a specific commit grouping
47 force commits to be grouped each <integer> seconds
49 produce a chart with gnuplot (default)
51 produce a chart with Google Charts
52 --chart-height=<integer>:
53 set the Google Charts height; the width is set
54 to a 4:3 ratio (default: 100)
62 print "Gathering data ...\n";
66 open my $fd, '-|', qw(git log --date=local), '--pretty=%ad%x00%s', @{$options->{cmdline}};
67 die "failed to get revs" unless $fd;
70 my ($date, $sub) = split '\0', $_, 2;
72 push @commits, str2time($date);
77 @commits = sort @commits;
79 print "...done, " . @commits . " commits.\n";
81 die "No commits!" if (@commits < 1);
83 my $gap = $commits[$#commits] - $commits[0];
86 if (defined $options->{step}) {
87 $step = $options->{step};
89 $step = $gap > $steps{yearly} ? 'monthly' :
90 $gap > $steps{monthly} ? 'weekly' :
91 $gap > $steps{weekly}/2 ? 'daily' :
94 $options->{step} = $step;
97 # truncate each commit (date) to the wanted step
98 # e.g. if the step is 'monthly', then commit YYYY/MM/DD
101 for my $commit (@commits) {
103 if (exists $trunc{$step}) {
104 # LOL -- no, seriously, is there a smarter way to do this?
105 $key = str2time(strftime($trunc{$step}, localtime($commit)));
106 } elsif ($step eq 'weekly') {
107 # horrible special case: do it daily, and then subtract
108 # as many days as necessary to go back to the previous sunday
109 # TODO config week start
110 $key = str2time(strftime($trunc{daily}, localtime($commit)));
111 my $wd = strftime("%w", localtime($commit));
112 $key -= $wd*$steps{daily};
114 $key = $commit - ($commit % $step);
116 if (exists $dataset{$key}) {
123 # fill missing steps and find max
124 my @keys = sort keys %dataset;
126 # ensure numeric step, taking into account
127 # that our numerical steps are approximate
129 if (exists $steps{$step}) {
130 $step = $steps{$step};
131 $tolerance = $step/2;
135 my $key = shift @keys;
137 $max = $dataset{$key} if $max < $dataset{$key};
138 my $next_step = $key + $step;
139 my $next = shift @keys;
141 while (abs($next - $next_step) > $tolerance) {
142 # next step is too far away
143 $dataset{$next_step} = 0;
149 $options->{max} = $max;
153 # functions to plot the datasets.
154 # each function can be called with either one or two parameters.
155 # when called with two parameters, the first is assumed to be the dataset, and the second the options
156 # (array and hash ref respectively).
157 # when called with a single parameter, it is assumed to be an options hash ref, and the dataset is
158 # created by calling gather_data with the passed options.
161 # TODO needs a lot of customization
162 sub google_chart($;$) {
165 if (! defined $options) {
167 $dataset = gather_data($options);
170 my $height=$options->{chart_height};
171 my $max = $options->{max};
173 my @keys = sort keys %$dataset;
175 my $to = $keys[@keys-1];
178 while (my $key = shift @keys) {
179 push @data, $dataset->{$key};
182 my $width=ceil(4*$height/3);
184 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";
186 my $launch = sprintf $url, join(",",@data);
188 # `git web--browse "$launch"`
192 sub gnuplot_chart($;$) {
195 if (! defined $options) {
197 $dataset = gather_data($options);
200 my @keys = sort keys %$dataset;
201 my $step=$options->{step};
203 my $to = $keys[$#keys];
206 while (my $key = shift @keys) {
207 $data .= "$key $dataset->{$key}\n";
209 my $max = $options->{max} + 1;
211 # add a fake datapoint lest to prevent the last
212 # datum from becoming invibile with style data steps
213 if (exists $steps{$step}) {
214 $to += $steps{$step};
220 # TODO allow customization
221 # in particular, detect (lack of) display and set term to dumb accordingly
222 my $termcmd = $options->{gnuplot_term};
224 my $plotsetup = $options->{gnuplot_setup};
225 $plotsetup .= "\nset yrange [0:$max]\n";
226 $plotsetup .= "set xrange ['$from':'$to']\n";
227 my ($formatx, $ticks);
228 if ($to - $from > $steps{yearly}) {
230 $ticks = $steps{yearly};
231 } elsif ($to - $from > $steps{monthly}) {
232 $formatx = "%b\\n%Y";
233 $ticks = $steps{monthly};
234 } elsif ($to - $from > $steps{weekly}/2) {
235 $formatx = "%d\\n%b";
236 $ticks = $steps{daily};
238 $formatx = "%H\\n%d";
239 $ticks = $steps{hourly};
241 $plotsetup .= "set format x \"$formatx\"\n";
242 $plotsetup .= "set xtics $ticks\n";
243 my $plotstyle = $options->{gnuplot_style};
244 my $plotoptions = $options->{gnuplot_plotwith};
246 open my $gp, "|gnuplot -persist";
248 my $gp_script = <<GPCMD
254 plot "-" using 1:2 $plotoptions
259 print STDOUT $gp_script;
260 print $gp $gp_script;
267 # charting/plotting options
268 plotter => \&gnuplot_chart,
271 gnuplot_setup => "set nokey",
272 gnuplot_style => 'set style data steps',
273 gnuplot_plotwith => '',
279 if (exists $steps{$key}) {
280 $options{step} = $key;
283 die "this can't happen ($key)" unless $key eq 'step';
285 if ($step =~/^\d+$/) {
286 $options{step} = 0 + $step;
289 if (exists $steps{$step}) {
290 $options{step} = $step;
293 die "unknown step $step";
298 # read our options first
299 Getopt::Long::Configure('pass_through');
301 'hourly' => \&parse_step,
302 'daily' => \&parse_step,
303 'weekly' => \&parse_step,
304 'monthly' => \&parse_step,
305 'yearly' => \&parse_step,
306 'step=s' => sub { parse_step(@_) },
307 'chart-height=i' => sub { $options{chart_height} = $_[1]},
308 google => sub { $options{plotter} = \&google_chart },
309 gnuplot => sub { $options{plotter} = \&gnuplot_chart },
318 # if anything was left, check for log options
320 $options{cmdline} = \@ARGV;
323 $options{plotter}->(\%options);