git-log-compact: pad multibyte initials properly
[git-log-compact.git] / git-log-compact
bloba1384608182afa85f6403a50e326887fa48e0282
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,2020 Kyle J. McKay <mackyle@gmail.com>. All rights reserved.
6 # License GPL v2
8 # Version 1.0.1
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 --initials=author,committer
33 Use author/committer initials and --two-initials
34 --initials=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 $width = shift;
76 my $wasutf8 = utf8::decode($initials);
77 $initials = lc($initials)." ";
78 $initials =~ s/[.]/ /g;
79 $initials =~ s/ iii? / /g;
80 $initials =~ s/ iv / /g;
81 $initials =~ s/ [js]r / /g;
82 $initials =~ s/[,;:'\042+_-]//g;
83 $initials =~ s/\([^(]*?\)/ /g;
84 $initials =~ s/\[[^[]*?\]/ /g;
85 $initials =~ s/\s+/ /g;
86 $initials =~ s/^ //g;
87 return "jc" if $iw == 2 && $initials eq "junio c hamano ";
88 $initials =~ s/([^ ])[^ ]* /$1/g;
89 if ($iw == 2) {
90 $initials =~ s/^(.).+(.)$/$1$2/;
91 } else {
92 $initials =~ s/^(..).+(.)$/$1$2/;
94 $initials .= " " x ($width - length($initials))
95 if length($initials) < $width;
96 utf8::encode($initials) if $wasutf8;
97 return $initials;
100 sub get_nocolor_indent {
101 my $indent = shift;
102 $indent =~ s/\033[^m]*m//g;
103 $indent =~ s/\s+$//;
104 $indent =~ s/-+\.$//;
105 return $indent;
108 sub get_blank_graph_indent {
109 my $indent = shift;
110 chomp $indent;
111 $indent =~ s/\033[^m]*m//g;
112 $indent =~ s/^[\s|]+//;
113 return $indent;
116 sub get_first_indent {
117 my $indent = shift;
118 $indent =~ s/\033[^m]*m//g;
119 $indent =~ s/./ /gs;
120 return $indent;
123 my $nobar;
124 my $barcolor;
125 my $resetcolor = "";
127 sub get_bar_color {
128 my ($prefix, $index) = @_;
129 my $c = (split(m{[-=^<>*+o /|\\_]}, $prefix))[$index];
130 $c =~ s/\Q$resetcolor\E//g if $resetcolor;
131 return $c;
134 sub get_indent {
135 my $indent = shift;
136 if ($nobar) {
137 $indent =~ tr/\-=^<>*+o./ /;
138 } else {
139 $indent =~ s/[-=^<>*+o]/$barcolor ? $barcolor."|".$resetcolor : "|"/e;
140 $indent =~ tr/\-./ /;
142 return $indent;
145 sub get_prefix {
146 my $indent = shift;
147 $indent =~ tr'\/'||';
148 return $indent;
151 sub get_defaults {
152 # defaults are cumulative, but an empty setting resets
153 my @defaults = ();
154 my $opts = qx(git config --get-all log-compact.defaults 2>/dev/null);
155 chomp($opts);
156 foreach (split(/\r\n|\r|\n/, $opts, -1)) {
157 s/^\s+//; s/\s+$//;
158 if ($_ eq "") {
159 @defaults = ();
160 next;
162 push(@defaults, $_);
164 return split(" ", join(" ", @defaults));
167 system("git rev-parse --git-dir >/dev/null") == 0 or exit(1);
168 my ($usemark, $usegraph, $usereflog, $useboundary, $useleftright, $usecherry, $setusecolor, $usecolor, $usecad);
169 my @args = ();
170 my $lastwasgrep;
171 my $dateopt = "%ct";
172 my $usewkday;
173 my $reflogsubj = "%gs";
174 my $sawdashdash;
175 $iw = undef;
176 my $iw2 = "";
177 my ($committer, $author, $ivar, $ivar2);
178 $ivar = \$author;
179 foreach my $arg (get_defaults(), @ARGV) {
180 my $nextisgrep;
181 if ($sawdashdash || $lastwasgrep) {
182 push(@args, $arg);
183 $lastwasgrep = $nextisgrep;
184 next;
186 if ($arg eq "-h") {
187 my $dash = "-";
188 my $exec_path = qx(git --exec-path 2>/dev/null);
189 chomp $exec_path;
190 $dash = " " if $ENV{PATH} =~ /^\Q$exec_path\E:/;
191 printf "$USAGE\n", $dash;
192 exit 0;
193 } elsif ($arg eq "--oneline") {
194 # silently ignore --oneline as we are always in a one line format
195 next;
196 } elsif ($arg eq "--seconds") {
197 # extra option
198 $timeformat = "%H:%M:%S";
199 next;
200 } elsif ($arg eq "--minutes") {
201 # extra option
202 $timeformat = "%H:%M";
203 next;
204 } elsif ($arg eq "--no-times") {
205 # extra option
206 $timeformat = "";
207 next;
208 } elsif ($arg eq "--two-initials") {
209 # extra option
210 $iw = 2;
211 next;
212 } elsif ($arg eq "--three-initials") {
213 # extra option
214 $iw = 3;
215 next;
216 } elsif ($arg eq "--no-initials") {
217 # extra option
218 $iw = 0;
219 next;
220 } elsif ($arg eq "--two-initials") {
221 # extra option
222 $iw = 3;
223 next;
224 } elsif ($arg eq "--commit-message") {
225 # extra option
226 $reflogsubj = "%s";
227 next;
228 } elsif ($arg eq "--author-date") {
229 # extra option
230 $dateopt = "%at";
231 $usecad = 1;
232 next;
233 } elsif ($arg eq "--committer-date") {
234 # extra option
235 $dateopt = "%ct";
236 $usecad = 1;
237 next;
238 } elsif ($arg eq "--weekday") {
239 # extra option
240 $usewkday = 1;
241 next;
242 } elsif ($arg eq "--no-weekday") {
243 # extra option
244 $usewkday = undef;
245 next;
246 } elsif ($arg =~ /^--initials=/) {
247 # extra option
248 $arg =~ s/^--initials=//;
249 if ($arg eq "author") {
250 $ivar = \$author;
251 $ivar2 = undef;
252 } elsif ($arg eq "committer") {
253 $ivar = \$committer;
254 $ivar2 = undef;
255 } elsif ($arg eq "committer,author" || $arg eq "committer/author") {
256 $ivar = \$committer;
257 $ivar2 = \$author;
258 } elsif ($arg eq "author,committer" || $arg eq "author/committer") {
259 $ivar = \$author;
260 $ivar2 = \$committer;
261 } else {
262 dodie "--initials= requires 'author', 'committer' or 'committer,author'";
264 next;
265 } elsif ($arg =~ /^--time-zone=/) {
266 # extra option
267 $arg =~ s/^--time-zone=//;
268 $ENV{TZ} = $arg;
269 next;
270 } elsif ($arg eq "--date-order" || $arg eq "--topo-order") {
271 $dateopt = "%ct" unless $usecad;
272 } elsif ($arg eq "--author-date-order") {
273 $dateopt = "%at";
274 } elsif ($arg =~ /^--(pretty|pretty=.*|format=.*|notes|show-notes|show-notes=.*|standard-notes)$/) {
275 dodie "formatting/notes option not allowed: $arg";
276 } elsif ($arg eq "--no-decorate" || $arg eq "--decorate=no") {
277 $setusedecorate = 1;
278 $usedecorate = undef;
279 } elsif ($arg eq "--decorate=auto") {
280 $setusedecorate = 1;
281 $usedecorate = -t STDOUT ? 1 : undef;
282 } elsif ($arg eq "--decorate" || $arg =~ /^--decorate=/) {
283 $setusedecorate = 1;
284 $usedecorate = 1;
285 } elsif ($arg eq "--color" || $arg eq "--color=always") {
286 $setusecolor = 1;
287 $usecolor = 1;
288 } elsif ($arg eq "--no-color" || $arg eq "--color=never") {
289 $setusecolor = 1;
290 $usecolor = undef;
291 } elsif ($arg eq "--color=auto") {
292 $setusecolor = 1;
293 $usecolor = -t STDOUT ? 1 : undef;
294 } elsif ($arg eq "-g" || $arg eq "--walk-reflogs") {
295 $usereflog = 1;
296 } elsif ($arg eq "--boundary") {
297 $useboundary = 1;
298 $usemark = 1;
299 } elsif ($arg eq "--cherry-mark" || $arg eq "--cherry") {
300 $usecherry = 1;
301 $usemark = 1;
302 } elsif ($arg eq "--left-right") {
303 $useleftright = 1;
304 $usemark = 1;
305 } elsif ($arg eq "--graph") {
306 $usegraph = 1;
307 } elsif ($arg =~ /^(--grep|--grep-reflog|-S|-G)$/) {
308 $nextisgrep = 1;
309 } elsif ($arg eq "--") {
310 $sawdashdash = 1;
312 push(@args, $arg);
313 $lastwasgrep = $nextisgrep;
315 $iw = defined($ivar2) ? 2 : 3 unless defined($iw);
316 $iw = "" if !$iw;
317 $iw2 = $iw if defined($ivar2);
318 my ($mark, $fixmark) = ("");
319 $mark = "%m " unless $usegraph || !$usemark;
320 if ($mark && !$useleftright) {
321 $fixmark = " ";
322 $fixmark = "+" if $usecherry;
325 my $color = "never";
326 my ($hashcolor, $datecolor, $timecolor, $initialscolor, $autocolor) = ("", "", "", "", "");
327 $usecolor = 1 if !$setusecolor && system("git", "config", "--get-colorbool", "color.diff") == 0;
328 if ($usecolor) {
329 $color = "always";
330 $autocolor = "%C(auto)";
331 $hashcolor= qx(git config --get-color color.diff.commit "yellow");
332 $datecolor= qx(git config --get-color color.log-compact.date "bold blue");
333 $timecolor= qx(git config --get-color color.log-compact.time "green") if $timeformat;
334 $initialscolor = qx(git config --get-color color.log-compact.initials "red") if $iw;
335 $resetcolor = qx(git config --get-color "" "reset");
337 my $decopt = "";
338 $decopt = "$autocolor%d" if use_decorate;
339 my $pager = qx(git var GIT_PAGER);
340 defined($pager) and chomp $pager;
341 $ENV{LESS} = "-FRX" unless exists $ENV{LESS};
342 $ENV{LV} = "-c" unless exists $ENV{LV};
344 my ($lastdate, $lastprefix, $lastplainprefix) = ("");
345 my $msgopt = "%s";
346 $msgopt = "%gd: $reflogsubj" if $usereflog;
347 my $lastwasroot = 1;
348 open(LOG, '-|', "git", "log", "--color=$color",
349 "--format=tformat:$mark%x1fCOMMIT %H %h $dateopt%x1f%cn%x1f%an%x1f%P%x1f$decopt $msgopt%x1f",
350 @args) or exit(1);
351 if (defined($pager) && $pager ne "cat") {
352 open OUT, "|$pager" or dodie "could not run pager \"$pager\": $!\n";
353 } else {
354 open OUT, '>&STDOUT' or die "could not dupe STDOUT: $!";
356 select((select(OUT),$|=1)[0]);
357 my $delblank;
358 my @lastparents = ();
359 my $lastwascommit;
360 my ($prefix, $data, $parentlist, $subject);
361 while (my $logline = <LOG>) {
362 ($prefix, $data, $committer, $author, $parentlist, $subject) = split(/\x1f/, $logline, -1);
363 $subject =~ s/ // if $subject;
364 my ($flag, $fullhash, $hash, $timestamp) = split(" ", $data, 4) if defined($data);
365 if (!defined($flag) || $flag ne "COMMIT") {
366 chomp $prefix;
367 $delblank = 0, next if $delblank && !$usegraph && $prefix =~ /^\s*$/;
368 $delblank = 0, next if $delblank && $usegraph && !get_blank_graph_indent($prefix);
369 print OUT "$prefix\n";
370 $lastprefix = $prefix;
371 $lastplainprefix = undef;
372 @lastparents = ();
373 $lastwascommit = undef;
374 next;
376 my $isroot = !$parentlist;
377 my @parents = split(' ', $parentlist) if $usegraph;
378 my $initials = $iw ? get_initials($$ivar, $iw) : "";
379 my $initials2 = $iw2 ? get_initials($$ivar2, $iw2) : "";
380 my ($newdate, $newday, $newtime) = split(" ", strftime("%Y-%m-%d %a $timeformat", localtime($timestamp)));
381 $newdate .= " " . $newday if $usewkday;
382 my $mightneedbreak = $lastwascommit && !$lastwasroot && $usegraph && !grep($_ eq $fullhash, @lastparents);
383 if ($lastdate ne $newdate || $mightneedbreak) {
384 my $indent = "";
385 if (!$lastdate || $mark) {
386 $indent = get_first_indent($prefix);
387 $lastprefix = $prefix;
388 $lastplainprefix = undef;
389 } elsif ($prefix ne "") {
390 my $newplainprefix = get_nocolor_indent($prefix);
391 defined($lastplainprefix) or $lastplainprefix = get_nocolor_indent($lastprefix);
392 $nobar = undef;
393 $barcolor = undef;
394 if ($newplainprefix =~ /^(.*?[-=^<>*+o])/) {{
395 my $marklen = length($1);
396 my $difflen = length($lastplainprefix) - length($1);
397 $nobar = 1;
398 if ($difflen >= 0) {
399 my $lastmark = substr($lastplainprefix, $marklen-1, 1);
400 $lastmark =~ /[-=^<>*+o]/ and $nobar = $lastwasroot || $mightneedbreak, last;
401 $lastmark eq "|" && $lastdate ne $newdate and
402 $nobar = 0,
403 $barcolor = get_bar_color($lastprefix, $marklen - 1),
404 last;
406 if ($lastdate eq $newdate) {
407 $lastprefix = $prefix;
408 $lastplainprefix = $newplainprefix;
409 goto NOBREAKNEEDED;
411 $difflen >= -1 or last;
412 substr($lastplainprefix, $marklen-2, 1) eq "\\" and
413 $nobar = 0,
414 $barcolor = get_bar_color($lastprefix, $marklen - 2),
415 last;
416 $difflen >= 1 &&
417 substr($lastplainprefix, $marklen, 1) eq "/" and
418 $nobar = 0,
419 $barcolor = get_bar_color($lastprefix, $marklen);
421 $indent = get_indent($prefix);
422 $lastprefix = $prefix;
423 $prefix = get_prefix($prefix);
424 $lastplainprefix = $newplainprefix;
426 if ($lastdate ne $newdate) {
427 printf OUT "%s%s=== %s ===%s\n", $indent,
428 $datecolor, $newdate, $resetcolor;
429 $lastdate = $newdate;
430 } else {
431 printf OUT "%s%s %s%s%-${iw}s%s%-${iw2}s %s\n", $indent,
432 ' ' x length($hash), ' ' x length($newtime),
433 ($iw ? " " : ""), "", ($iw2 ? " " : ""), "",
434 "..........";
436 } else {
437 $lastprefix = $prefix;
438 $lastplainprefix = undef;
440 NOBREAKNEEDED:
441 $lastwasroot = $isroot;
442 @lastparents = @parents;
443 $lastwascommit = 1;
444 my $rootflag = " ";
445 if ($isroot) {
446 $rootflag = "_";
447 $prefix = substr($prefix, 0, length($prefix) - 1) . "_"
448 if length($prefix);
449 if ($prefix =~ /^(.*?[-=^<>*+o])(.+)$/) {
450 my ($initial, $trail) = ($1, $2);
451 $trail =~ tr/ /_/;
452 $prefix = $initial . $trail;
455 if ($fixmark) {
456 $prefix = $fixmark . substr($prefix, 1)
457 if $prefix =~ /^[<>]/;
459 printf OUT "%s%s%s%s%s%s%s%s%s%s\n", $prefix,
460 "$hashcolor$hash$resetcolor",
461 $rootflag, ($timeformat ? "$timecolor$newtime$resetcolor " : ""),
462 $initialscolor, $initials, ($iw2 ? "/" : ""), $initials2,
463 ($iw ? "$resetcolor " : ""), $subject;
464 $delblank = 1;
466 close LOG;
467 close OUT;