git log --oneline alternative with dates, times and initials
[git-log-compact.git] / git-log-compact
blob8c8636d7c447584f8944f9feaaa84fe80735f818
1 #!/usr/bin/env perl
3 # git-log-compact.pl -- compact git log --oneline alternative with dates, times and initials
4 # Copyright (C) 2015,2016 Kyle J. McKay <mackyle@gmail.com>. All rights reserved.
6 # License GPL v2
8 # Version 1.0
10 use 5.008;
11 use strict;
12 use warnings;
13 use File::Basename qw(basename);
14 use POSIX qw(strftime _exit);
15 use Encode;
17 my $USAGE = <<'USAGE';
18 usage: git%slog-compact [<options>] [<revision-range>] [[--] <path>...]
20 -h Show this help
21 --seconds Use HH:MM:SS instead of just the default HH:MM
22 --minutes Use just HH:MM (default) for times not HH:MM:SS
23 --no-times Omit the time field entirely
24 --two-initials Use maximum of two initials instead of default three
25 --three-initials Use maximum of three initials (default)
26 --no-initials Omit the initials field entirely
27 --commit-message Show the commit message when using --walk-reflogs
28 --author-date Use author dates and times
29 --committer-date Use committer dates and times (default)
30 --initials=author Use author initials (default)
31 --initials=committer Use committer initials
32 --intiials=author,committer
33 Use author/committer initials and --two-initials
34 --intiials=committer,author
35 Use committer/author initials and --two-initials
36 --time-zone=<zone> Set TZ environment variable to <zone>
37 --weekday Show the weekday with the date
38 --no-weekday Do not show the weekday with the date (default)
40 other log options See `git help log` for more information
42 Default colors for dates, times and initials may be changed by setting
43 `color.log-compact.date`, `color.log-compact.time` and/or
44 `color.log-compact.initials` config values. Dates and times are shown in the
45 local time zone if TZ is not set in the environment and the `--time-zone`
46 option has not been used. Default options may be set in the
47 `log-compact.defaults` config value and they will be treated as though they were
48 listed first in the command line options list (e.g.
49 `git config log-compact.defaults "--abbrev=8 --seconds"`)
50 USAGE
52 my $timeformat = "%H:%M";
54 $SIG{PIPE} = sub {_exit 1};
56 sub dodie {
57 my $msg = join(" ", @_);
58 chomp $msg;
59 die basename($0).": fatal: ".$msg."\n";
62 my ($setusedecorate, $usedecorate);
63 sub use_decorate {
64 return $usedecorate if $setusedecorate;
65 my $do = qx(git config --get log.decorate 2>/dev/null) || "0";
66 chomp $do;
67 return 0 if $do eq "0" || $do eq "false" || $do eq "off";
68 return 0 if $do eq "auto" && ! -t STDOUT;
69 return 1;
72 my $iw;
73 sub get_initials {
74 my $initials = shift;
75 my $wasutf8 = utf8::decode($initials);
76 $initials = lc($initials)." ";
77 $initials =~ s/[.]/ /g;
78 $initials =~ s/ iii? / /g;
79 $initials =~ s/ iv / /g;
80 $initials =~ s/ [js]r / /g;
81 $initials =~ s/[,;:'\042+_-]//g;
82 $initials =~ s/\([^(]*?\)/ /g;
83 $initials =~ s/\[[^[]*?\]/ /g;
84 $initials =~ s/\s+/ /g;
85 $initials =~ s/^ //g;
86 return "jc" if $iw == 2 && $initials eq "junio c hamano ";
87 $initials =~ s/([^ ])[^ ]* /$1/g;
88 if ($iw == 2) {
89 $initials =~ s/^(.).+(.)$/$1$2/;
90 } else {
91 $initials =~ s/^(..).+(.)$/$1$2/;
93 utf8::encode($initials) if $wasutf8;
94 return $initials;
97 sub get_nocolor_indent {
98 my $indent = shift;
99 $indent =~ s/\033[^m]*m//g;
100 $indent =~ s/\s+$//;
101 $indent =~ s/-+\.$//;
102 return $indent;
105 sub get_blank_graph_indent {
106 my $indent = shift;
107 chomp $indent;
108 $indent =~ s/\033[^m]*m//g;
109 $indent =~ s/^[\s|]+//;
110 return $indent;
113 sub get_first_indent {
114 my $indent = shift;
115 $indent =~ s/\033[^m]*m//g;
116 $indent =~ s/./ /gs;
117 return $indent;
120 my $nobar;
121 my $barcolor;
122 my $resetcolor = "";
124 sub get_bar_color {
125 my ($prefix, $index) = @_;
126 my $c = (split(m{[-=^<>*+o /|\\_]}, $prefix))[$index];
127 $c =~ s/\Q$resetcolor\E//g if $resetcolor;
128 return $c;
131 sub get_indent {
132 my $indent = shift;
133 if ($nobar) {
134 $indent =~ tr/\-=^<>*+o./ /;
135 } else {
136 $indent =~ s/[-=^<>*+o]/$barcolor ? $barcolor."|".$resetcolor : "|"/e;
137 $indent =~ tr/\-./ /;
139 return $indent;
142 sub get_prefix {
143 my $indent = shift;
144 $indent =~ tr'\/'||';
145 return $indent;
148 sub get_defaults {
149 # defaults are cumulative, but an empty setting resets
150 my @defaults = ();
151 my $opts = qx(git config --get-all log-compact.defaults 2>/dev/null);
152 chomp($opts);
153 foreach (split(/\r\n|\r|\n/, $opts, -1)) {
154 s/^\s+//; s/\s+$//;
155 if ($_ eq "") {
156 @defaults = ();
157 next;
159 push(@defaults, $_);
161 return split(" ", join(" ", @defaults));
164 system("git rev-parse --git-dir >/dev/null") == 0 or exit(1);
165 my ($usemark, $usegraph, $usereflog, $useboundary, $useleftright, $usecherry, $setusecolor, $usecolor, $usecad);
166 my @args = ();
167 my $lastwasgrep;
168 my $dateopt = "%ct";
169 my $usewkday;
170 my $reflogsubj = "%gs";
171 my $sawdashdash;
172 $iw = undef;
173 my $iw2 = "";
174 my ($committer, $author, $ivar, $ivar2);
175 $ivar = \$author;
176 foreach my $arg (get_defaults(), @ARGV) {
177 my $nextisgrep;
178 if ($sawdashdash || $lastwasgrep) {
179 push(@args, $arg);
180 $lastwasgrep = $nextisgrep;
181 next;
183 if ($arg eq "-h") {
184 my $dash = "-";
185 my $exec_path = qx(git --exec-path 2>/dev/null);
186 chomp $exec_path;
187 $dash = " " if $ENV{PATH} =~ /^\Q$exec_path\E:/;
188 printf "$USAGE\n", $dash;
189 exit 0;
190 } elsif ($arg eq "--oneline") {
191 # silently ignore --oneline as we are always in a one line format
192 next;
193 } elsif ($arg eq "--seconds") {
194 # extra option
195 $timeformat = "%H:%M:%S";
196 next;
197 } elsif ($arg eq "--minutes") {
198 # extra option
199 $timeformat = "%H:%M";
200 next;
201 } elsif ($arg eq "--no-times") {
202 # extra option
203 $timeformat = "";
204 next;
205 } elsif ($arg eq "--two-initials") {
206 # extra option
207 $iw = 2;
208 next;
209 } elsif ($arg eq "--three-initials") {
210 # extra option
211 $iw = 3;
212 next;
213 } elsif ($arg eq "--no-initials") {
214 # extra option
215 $iw = 0;
216 next;
217 } elsif ($arg eq "--two-initials") {
218 # extra option
219 $iw = 3;
220 next;
221 } elsif ($arg eq "--commit-message") {
222 # extra option
223 $reflogsubj = "%s";
224 next;
225 } elsif ($arg eq "--author-date") {
226 # extra option
227 $dateopt = "%at";
228 $usecad = 1;
229 next;
230 } elsif ($arg eq "--committer-date") {
231 # extra option
232 $dateopt = "%ct";
233 $usecad = 1;
234 next;
235 } elsif ($arg eq "--weekday") {
236 # extra option
237 $usewkday = 1;
238 next;
239 } elsif ($arg eq "--no-weekday") {
240 # extra option
241 $usewkday = undef;
242 next;
243 } elsif ($arg =~ /^--initials=/) {
244 # extra option
245 $arg =~ s/^--initials=//;
246 if ($arg eq "author") {
247 $ivar = \$author;
248 $ivar2 = undef;
249 } elsif ($arg eq "committer") {
250 $ivar = \$committer;
251 $ivar2 = undef;
252 } elsif ($arg eq "committer,author" || $arg eq "committer/author") {
253 $ivar = \$committer;
254 $ivar2 = \$author;
255 } elsif ($arg eq "author,committer" || $arg eq "author/committer") {
256 $ivar = \$author;
257 $ivar2 = \$committer;
258 } else {
259 dodie "--initials= requires 'author', 'committer' or 'committer,author'";
261 next;
262 } elsif ($arg =~ /^--time-zone=/) {
263 # extra option
264 $arg =~ s/^--time-zone=//;
265 $ENV{TZ} = $arg;
266 next;
267 } elsif ($arg eq "--date-order" || $arg eq "--topo-order") {
268 $dateopt = "%ct" unless $usecad;
269 } elsif ($arg eq "--author-date-order") {
270 $dateopt = "%at";
271 } elsif ($arg =~ /^--(pretty|pretty=.*|format=.*|notes|show-notes|show-notes=.*|standard-notes)$/) {
272 dodie "formatting/notes option not allowed: $arg";
273 } elsif ($arg eq "--no-decorate" || $arg eq "--decorate=no") {
274 $setusedecorate = 1;
275 $usedecorate = undef;
276 } elsif ($arg eq "--decorate=auto") {
277 $setusedecorate = 1;
278 $usedecorate = -t STDOUT ? 1 : undef;
279 } elsif ($arg eq "--decorate" || $arg =~ /^--decorate=/) {
280 $setusedecorate = 1;
281 $usedecorate = 1;
282 } elsif ($arg eq "--color" || $arg eq "--color=always") {
283 $setusecolor = 1;
284 $usecolor = 1;
285 } elsif ($arg eq "--no-color" || $arg eq "--color=never") {
286 $setusecolor = 1;
287 $usecolor = undef;
288 } elsif ($arg eq "--color=auto") {
289 $setusecolor = 1;
290 $usecolor = -t STDOUT ? 1 : undef;
291 } elsif ($arg eq "-g" || $arg eq "--walk-reflogs") {
292 $usereflog = 1;
293 } elsif ($arg eq "--boundary") {
294 $useboundary = 1;
295 $usemark = 1;
296 } elsif ($arg eq "--cherry-mark" || $arg eq "--cherry") {
297 $usecherry = 1;
298 $usemark = 1;
299 } elsif ($arg eq "--left-right") {
300 $useleftright = 1;
301 $usemark = 1;
302 } elsif ($arg eq "--graph") {
303 $usegraph = 1;
304 } elsif ($arg =~ /^(--grep|--grep-reflog|-S|-G)$/) {
305 $nextisgrep = 1;
306 } elsif ($arg eq "--") {
307 $sawdashdash = 1;
309 push(@args, $arg);
310 $lastwasgrep = $nextisgrep;
312 $iw = defined($ivar2) ? 2 : 3 unless defined($iw);
313 $iw = "" if !$iw;
314 $iw2 = $iw if defined($ivar2);
315 my ($mark, $fixmark) = ("");
316 $mark = "%m " unless $usegraph || !$usemark;
317 if ($mark && !$useleftright) {
318 $fixmark = " ";
319 $fixmark = "+" if $usecherry;
322 my $color = "never";
323 my ($hashcolor, $datecolor, $timecolor, $initialscolor, $autocolor) = ("", "", "", "", "");
324 $usecolor = 1 if !$setusecolor && system("git", "config", "--get-colorbool", "color.diff") == 0;
325 if ($usecolor) {
326 $color = "always";
327 $autocolor = "%C(auto)";
328 $hashcolor= qx(git config --get-color color.diff.commit "yellow");
329 $datecolor= qx(git config --get-color color.log-compact.date "bold blue");
330 $timecolor= qx(git config --get-color color.log-compact.time "green") if $timeformat;
331 $initialscolor = qx(git config --get-color color.log-compact.initials "red") if $iw;
332 $resetcolor = qx(git config --get-color "" "reset");
334 my $decopt = "";
335 $decopt = "$autocolor%d" if use_decorate;
336 my $pager = qx(git var GIT_PAGER);
337 defined($pager) and chomp $pager;
338 $ENV{LESS} = "-FRX" unless exists $ENV{LESS};
339 $ENV{LV} = "-c" unless exists $ENV{LV};
341 my ($lastdate, $lastprefix, $lastplainprefix) = ("");
342 my $msgopt = "%s";
343 $msgopt = "%gd: $reflogsubj" if $usereflog;
344 my $lastwasroot = 1;
345 open(LOG, '-|', "git", "log", "--color=$color",
346 "--format=tformat:$mark%x1fCOMMIT %H %h $dateopt%x1f%cn%x1f%an%x1f%P%x1f$decopt $msgopt%x1f",
347 @args) or exit(1);
348 if (defined($pager) && $pager ne "cat") {
349 open OUT, "|$pager" or dodie "could not run pager \"$pager\": $!\n";
350 } else {
351 open OUT, '>&STDOUT' or die "could not dupe STDOUT: $!";
353 select((select(OUT),$|=1)[0]);
354 my $delblank;
355 my @lastparents = ();
356 my $lastwascommit;
357 my ($prefix, $data, $parentlist, $subject);
358 while (my $logline = <LOG>) {
359 ($prefix, $data, $committer, $author, $parentlist, $subject) = split(/\x1f/, $logline, -1);
360 $subject =~ s/ // if $subject;
361 my ($flag, $fullhash, $hash, $timestamp) = split(" ", $data, 4) if defined($data);
362 if (!defined($flag) || $flag ne "COMMIT") {
363 chomp $prefix;
364 $delblank = 0, next if $delblank && !$usegraph && $prefix =~ /^\s*$/;
365 $delblank = 0, next if $delblank && $usegraph && !get_blank_graph_indent($prefix);
366 print OUT "$prefix\n";
367 $lastprefix = $prefix;
368 $lastplainprefix = undef;
369 @lastparents = ();
370 $lastwascommit = undef;
371 next;
373 my $isroot = !$parentlist;
374 my @parents = split(' ', $parentlist) if $usegraph;
375 my $initials = $iw ? get_initials($$ivar) : "";
376 my $initials2 = $iw2 ? get_initials($$ivar2) : "";
377 my ($newdate, $newday, $newtime) = split(" ", strftime("%Y-%m-%d %a $timeformat", localtime($timestamp)));
378 $newdate .= " " . $newday if $usewkday;
379 my $mightneedbreak = $lastwascommit && !$lastwasroot && $usegraph && !grep($_ eq $fullhash, @lastparents);
380 if ($lastdate ne $newdate || $mightneedbreak) {
381 my $indent = "";
382 if (!$lastdate || $mark) {
383 $indent = get_first_indent($prefix);
384 $lastprefix = $prefix;
385 $lastplainprefix = undef;
386 } elsif ($prefix ne "") {
387 my $newplainprefix = get_nocolor_indent($prefix);
388 defined($lastplainprefix) or $lastplainprefix = get_nocolor_indent($lastprefix);
389 $nobar = undef;
390 $barcolor = undef;
391 if ($newplainprefix =~ /^(.*?[-=^<>*+o])/) {{
392 my $marklen = length($1);
393 my $difflen = length($lastplainprefix) - length($1);
394 $nobar = 1;
395 if ($difflen >= 0) {
396 my $lastmark = substr($lastplainprefix, $marklen-1, 1);
397 $lastmark =~ /[-=^<>*+o]/ and $nobar = $lastwasroot || $mightneedbreak, last;
398 $lastmark eq "|" && $lastdate ne $newdate and
399 $nobar = 0,
400 $barcolor = get_bar_color($lastprefix, $marklen - 1),
401 last;
403 if ($lastdate eq $newdate) {
404 $lastprefix = $prefix;
405 $lastplainprefix = $newplainprefix;
406 goto NOBREAKNEEDED;
408 $difflen >= -1 or last;
409 substr($lastplainprefix, $marklen-2, 1) eq "\\" and
410 $nobar = 0,
411 $barcolor = get_bar_color($lastprefix, $marklen - 2),
412 last;
413 $difflen >= 1 &&
414 substr($lastplainprefix, $marklen, 1) eq "/" and
415 $nobar = 0,
416 $barcolor = get_bar_color($lastprefix, $marklen);
418 $indent = get_indent($prefix);
419 $lastprefix = $prefix;
420 $prefix = get_prefix($prefix);
421 $lastplainprefix = $newplainprefix;
423 if ($lastdate ne $newdate) {
424 printf OUT "%s%s=== %s ===%s\n", $indent,
425 $datecolor, $newdate, $resetcolor;
426 $lastdate = $newdate;
427 } else {
428 printf OUT "%s%s %s%s%-${iw}s%s%-${iw2}s %s\n", $indent,
429 ' ' x length($hash), ' ' x length($newtime),
430 ($iw ? " " : ""), "", ($iw2 ? " " : ""), "",
431 "..........";
433 } else {
434 $lastprefix = $prefix;
435 $lastplainprefix = undef;
437 NOBREAKNEEDED:
438 $lastwasroot = $isroot;
439 @lastparents = @parents;
440 $lastwascommit = 1;
441 my $rootflag = " ";
442 if ($isroot) {
443 $rootflag = "_";
444 $prefix = substr($prefix, 0, length($prefix) - 1) . "_"
445 if length($prefix);
446 if ($prefix =~ /^(.*?[-=^<>*+o])(.+)$/) {
447 my ($initial, $trail) = ($1, $2);
448 $trail =~ tr/ /_/;
449 $prefix = $initial . $trail;
452 if ($fixmark) {
453 $prefix = $fixmark . substr($prefix, 1)
454 if $prefix =~ /^[<>]/;
456 printf OUT "%s%s%s%s%s%-${iw}s%s%-${iw2}s%s%s\n", $prefix,
457 "$hashcolor$hash$resetcolor",
458 $rootflag, ($timeformat ? "$timecolor$newtime$resetcolor " : ""),
459 $initialscolor, $initials, ($iw2 ? "/" : ""), $initials2,
460 ($iw ? "$resetcolor " : ""), $subject;
461 $delblank = 1;
463 close LOG;
464 close OUT;