From d28bb6099f474a917d8f332551ce6bf89b77212e Mon Sep 17 00:00:00 2001 From: Fernando Schauenburg Date: Sun, 14 Jul 2024 02:33:19 +0200 Subject: [PATCH] git: add some useful commands (authors, cal, ignored, tree) --- bin/git-authors | 9 + bin/git-cal | 516 ++++++++++++++++++++++++++++++++++++++++++++++++ bin/git-ignored | 6 + bin/git-tree | 10 + 4 files changed, 541 insertions(+) create mode 100755 bin/git-authors create mode 100755 bin/git-cal create mode 100755 bin/git-ignored create mode 100755 bin/git-tree diff --git a/bin/git-authors b/bin/git-authors new file mode 100755 index 0000000..b9288a0 --- /dev/null +++ b/bin/git-authors @@ -0,0 +1,9 @@ +#!/bin/sh +# +# List authors in the repository. +# +exec git shortlog --email --numbered --summary "$@" \ + | awk '{$1=""; sub(" ", ""); print}' \ + | awk -F'<' '!seen[$1]++' \ + | awk -F'<' '!seen[$2]++' + diff --git a/bin/git-cal b/bin/git-cal new file mode 100755 index 0000000..3cc52b3 --- /dev/null +++ b/bin/git-cal @@ -0,0 +1,516 @@ +#!/usr/bin/perl +# +# Github like contributions calendar on terminal. +# +# Source: https://github.com/k4rthik/git-cal +# +use strict; +use warnings; + +use utf8; +use Getopt::Long; +use Pod::Usage; +use Time::Local; +use Data::Dumper; + +binmode(STDOUT, ":utf8"); +#command line options +my ( $help, $period, $use_ascii, $use_ansi, $use_unicode, $format, $author, $filepath, $all_branches ); + +GetOptions( + 'help|?' => \$help, + 'period|p=n' => \$period, + 'ascii' => \$use_ascii, + 'ansi' => \$use_ansi, + 'unicode' => \$use_unicode, + 'author=s' => \$author, + 'all' => \$all_branches +) or pod2usage(2); + +pod2usage(1) if $help; + +$filepath = shift @ARGV; + +# also tried to use unicode chars instead of colors, the exp did not go well +#qw(⬚ ⬜ ▤ ▣ ⬛) +#qw(⬚ ▢ ▤ ▣ ⬛) + +my @unicode = qw(⬚ ▢ ▤ ▣ ⬛); +# my @colors = ( 237, 139, 40, 190, 1 ); original +my @colors = ( 235, 58, 64, 70, 82 ); +my @ascii = ( " ", ".", "o", "O", "0" ); +my @months = qw (Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); + +configure(\$period, \$use_ascii, \$use_ansi, \$use_unicode, \$author); + +process(); + +# 53 X 7 grid +# consists of 0 - 370 blocks +my ( @grid, @timeline, %pos_month, %month_pos, $jan1, $cur_year, $max_epoch, $min_epoch, $max_commits, $q1, $q2, $q3 ); +my ( $first_block, $last_block, $start_block, $end_block, $row_start, $row_end ); +#loads of global variables + +sub process { + $format + = $use_ansi ? 'ansi' + : $use_ascii ? 'ascii' + : $use_unicode ? 'unicode' + : undef; + + #if the user decided not to choose the format, let's pick some environmentally smart format + if ( !defined $format ) { + $format + = $ENV{EMACS} ? 'unicode' + : $ENV{TERM} eq 'dumb' ? 'ascii' + : 'ansi'; + } + init_cal_stuff(); + process_current_repo(); + my %stats = compute_stats(); + print_grid(%stats); +} + +sub process_current_repo { + my $git_command = git_command(); + my @epochs = qx/$git_command/; + + if ($?) { + print "fatal: git-cal failed to get the git log\n"; + exit(2); + } + + if ( !@epochs ) { + print "git-cal: got empty log, nothing to do\n"; + exit(1); + } + my $status; + foreach (@epochs) { + $status = add_epoch($_); + last if !$status; + } +} + +sub git_command { + my $command = qq{git log --no-merges --pretty=format:"%at" --since="13 months"}; + $command .= qq{ --author="$author"} if $author; + $command .= qq{ --all } if $all_branches; + if ($filepath) { + if ( -e $filepath ) { + $command .= qq{ -- $filepath}; + } + else { + print "fatal: $filepath: no such file or directory\n"; + exit(2); + } + } + return $command; +} + +sub init_cal_stuff { + my ( $wday, $yday, $month, $year ) = ( localtime(time) )[ 6, 7, 4, 5 ]; + $cur_year = $year; + $jan1 = 370 - ( $yday + 6 - $wday ); + $last_block = $jan1 + $yday + 1; + $first_block = $last_block - 365; + $max_commits = 0; + push @timeline, $jan1; + $month_pos{0} = $jan1; + my $cur = $jan1; + + foreach ( 0 .. $month - 1 ) { + $cur += number_of_days( $_, $year ); + push @timeline, $cur; + $month_pos{ $_ + 1 } = $cur; + } + $cur = $jan1; + for ( my $m = 11; $m > $month; $m-- ) { + $cur -= number_of_days( $m, $year - 1 ); + unshift @timeline, $cur; + $month_pos{$m} = $cur; + } + + $pos_month{ $month_pos{$_} } = $months[$_] foreach keys %month_pos; + + die "period can only be between -11 to 0 and 1 to 12" if ( defined $period && ( $period < -11 || $period > 12 ) ); + if ( !defined $period ) { + $start_block = $first_block; + $end_block = $last_block; + } + elsif ( $period > 0 ) { + $start_block = $month_pos{ $period - 1 }; + $end_block = $month_pos{ $period % 12 }; + $end_block = $last_block if $start_block > $end_block; + } + else { + $start_block = $timeline[ 11 + $period ]; + $start_block = $first_block if $period == -12; + $end_block = $last_block; + } + $row_start = int $start_block / 7; + $row_end = int $end_block / 7; + $max_epoch = time - 86400 * ( $last_block - $end_block ); + $min_epoch = time - 86400 * ( $last_block - $start_block ); + +} + + +sub add_epoch { + my ($epoch, $count) = @_; + if ( $epoch > $max_epoch || $epoch < $min_epoch ) { + return 1; + } + my ( $month, $year, $wday, $yday ) = ( localtime($epoch) )[ 4, 5, 6, 7 ]; + my $pos; + if ( $year == $cur_year ) { + $pos = ( $jan1 + $yday ); + } + else { + my $total = ( $year % 4 ) ? 365 : 366; + $pos = ( $jan1 - ( $total - $yday ) ); + } + return 0 if $pos < 0; #just in case + add_to_grid( $pos, $epoch, $count ); + return 1; +} + +sub add_to_grid { + my ( $pos, $epoch, $count ) = @_; + $count ||= 1; + my $r = int $pos / 7; + my $c = $pos % 7; + $grid[$r][$c]->{commits}+=$count; + $grid[$r][$c]->{epoch} = $epoch; + $max_commits = $grid[$r][$c]->{commits} if $grid[$r][$c]->{commits} > $max_commits; +} + + +sub compute_stats { + my %commit_counts; + + my ( + $total_commits, + $cur_streak, + $cur_start, + $max_streak, + $max_start, + $max_end, + $cur_streak_weekdays, + $cur_weekdays_start, + $max_streak_weekdays, + $max_weekdays_start, + $max_weekdays_end, + $q1, + $q2, + $q3, + ) = (0) x 14; + + foreach my $r ( $row_start .. $row_end ) { + foreach my $c ( 0 .. 6 ) { + my $cur_block = ( $r * 7 ) + $c; + if ( $cur_block >= $start_block && $cur_block < $end_block ) { + my $count = $grid[$r][$c]->{commits} || 0; + $total_commits += $count; + if ($count) { + $commit_counts{$count} = 1; + $cur_streak++; + $cur_start ||= $grid[$r][$c]->{epoch}; + if ( $cur_streak > $max_streak ) { + $max_streak = $cur_streak; + $max_start = $cur_start; + $max_end = $grid[$r][$c]->{epoch}; + } + + #count++ if you work on weekends and streak will not be broken otherwise :) + $cur_streak_weekdays++; + $cur_weekdays_start ||= $grid[$r][$c]->{epoch}; + if ( $cur_streak_weekdays > $max_streak_weekdays ) { + $max_streak_weekdays = $cur_streak_weekdays; + $max_weekdays_start = $cur_weekdays_start; + $max_weekdays_end = $grid[$r][$c]->{epoch}; + } + } + else { + $cur_streak = 0; + $cur_start = 0; + if ( $c > 0 && $c < 6 ) { + $cur_streak_weekdays = 0; + $cur_weekdays_start = 0; + } + } + } + } + } + + #now compute quartiles + my @commit_counts = sort { $a <=> $b } ( keys %commit_counts ); + $q1 = $commit_counts[ int( scalar @commit_counts ) / 4 ]; + $q2 = $commit_counts[ int( scalar @commit_counts ) / 2 ]; + $q3 = $commit_counts[ int( 3 * ( scalar @commit_counts ) / 4 ) ]; + + #print "commit counts: " . (scalar @commit_counts) . " - " . (join ",",@commit_counts) . "\n\n"; + #print "quartiles: $q1 $q2 $q3\n"; + + #die Dumper \%stat; + + my %stat = ( + total_commits => $total_commits, + cur_streak => $cur_streak , + cur_start => $cur_start , + max_streak => $max_streak , + max_start => $max_start , + max_end => $max_end , + cur_streak_weekdays => $cur_streak_weekdays, + cur_weekdays_start => $cur_weekdays_start, + max_streak_weekdays => $max_streak_weekdays, + max_weekdays_start => $max_weekdays_start, + max_weekdays_end => $max_weekdays_end , + q1 => $q1, + q2 => $q2, + q3 => $q3, + ); + + + return %stat; +} + +sub print_grid { + my %stat = @_; + + my $space = 6; + print_month_names($space); + foreach my $c ( 0 .. 6 ) { + printf "\n%" . ( $space - 2 ) . "s", ""; + + print $c == 1 ? "M " + : $c == 3 ? "W " + : $c == 5 ? "F " + : " "; + + foreach my $r ( $row_start .. $row_end ) { + my $cur_block = ( $r * 7 ) + $c; + if ( $cur_block >= $start_block && $cur_block < $end_block ) { + my $val = $grid[$r][$c]->{commits} || 0; + + my $index = $val == 0 ? 0 + : $val <= $stat{q1} ? 1 + : $val <= $stat{q2} ? 2 + : $val <= $stat{q3} ? 3 + : 4; + + print_block($index); + } + else { + print " "; + } + } + } + print "\n\n"; + printf " %d total commits\t\t\tLess ", $stat{total_commits}; + print_block($_) foreach ( 0 .. 4 ); + printf " More\n\n"; +} + + +sub print_block { + my $index = shift; + $index = 4 if $index > 4; + $_ + = ( $format eq "ascii" ) ? "${ascii[$index]} " + : ( $format eq "unicode" ) ? "${unicode[$index]} " + : "\e[38;5;$colors[$index]m\x{25fc} \e[0m"; + print; +} + + +sub print_month_names { + #print month labels, printing current month in the right position is tricky + my $space = shift; + if ( defined $period && $period > 0 ) { + printf "%" . $space . "s %3s", "", $months[ $period - 1 ]; + return; + } + my $label_printer = 0; + my $timeline_iter = defined $period ? 11 + $period : 0; + if ( $start_block == $first_block && $timeline[0] != 0 ) { + my $first_pos = int $timeline[0] / 7; + if ( $first_pos == 0 ) { + printf "%" . ( $space - 2 ) . "s", ""; + print $pos_month{ $timeline[-1] } . " "; + print $pos_month{ $timeline[0] } . " "; + $timeline_iter++; + } + elsif ( $first_pos == 1 ) { + printf "%" . ( $space - 2 ) . "s", ""; + print $pos_month{ $timeline[-1] } . " "; + } + else { + printf "%" . $space . "s", ""; + printf "%-" . ( 2 * $first_pos ) . "s", $pos_month{ $timeline[-1] }; + } + $label_printer = $first_pos; + } + else { + printf "%" . $space . "s", ""; + $label_printer += ( int $start_block / 7 ); + } + + while ( $label_printer < $end_block / 7 && $timeline_iter <= $#timeline ) { + while ( ( int $timeline[$timeline_iter] / 7 ) != $label_printer ) { print " "; $label_printer++; } + print " " . $pos_month{ $timeline[$timeline_iter] } . " "; + $label_printer += 3; + $timeline_iter++; + } +} + +sub print_message { + my ( $days, $start_epoch, $end_epoch, $message ) = @_; + if ($days) { + my @range; + foreach my $epoch ( $start_epoch, $end_epoch ) { + my ( $mday, $mon, $year ) = ( localtime($epoch) )[ 3, 4, 5 ]; + my $s = sprintf( "%3s %2d %4d", $months[$mon], $mday, ( 1900 + $year ) ); + push @range, $s; + } + printf "%4d: Days ( %-25s ) - %-40s\n", $days, ( join " - ", @range ), $message; + } + else { + printf "%4d: Days - %-40s\n", $days, $message; + } +} + +sub number_of_days { + my ( $month, $year ) = @_; + return 30 if $month == 3 || $month == 5 || $month == 8 || $month == 10; + return 31 if $month != 1; + return 28 if $year % 4; + return 29; +} + +sub configure { + my ($period, $ascii, $ansi, $unicode, $author) = @_; + my @wanted; + push @wanted, 'format' if (not grep { defined $$_ } ($ascii, $ansi, $unicode)); + push @wanted, 'period' if (not defined $$period); + push @wanted, 'author' if (not defined $$author); + if (@wanted) { + my $git_command = "git config --get-regexp 'calendar\.*'"; + my @parts = split(/\s/, qx/$git_command/); + if(@parts) { + my %config; + while(my ($key, $value) = splice(@parts, 0, 2)) { + $key =~ s/calendar\.//; + $config{$key} = $value; + } + local @ARGV = (map { ( "-$_" => $config{$_} ) } + grep { exists $config{$_} } @wanted); + GetOptions( + 'period=n' => $period, + 'format=s' => sub { + if ($_[1] eq 'ascii') { $$ascii ||= 1; } + elsif ($_[1] eq 'ansi') { $$ansi ||= 1; } + elsif ($_[1] eq 'unicode') { $$unicode ||= 1; } + }, + 'author=s' => $author + ); + } + } +} + +__END__ + +=head1 NAME + +git-cal - A simple tool to view commits calendar (similar to github contributions calendar) on command line + +=head1 SYNOPSIS + +"git-cal" is a tool to visualize the git commit history in github's contribution calendar style. +The calendar shows how frequently the commits are made over the past year or some choosen period +Activity can be displayed using ascii, ansi or unicode characters, default is choosen based on ENV + + git-cal + + git-cal --period=<1..12, -11..0> + + git-cal --author= + + git-cal --ascii + + git-cal --ansi + + git-cal --unicode + + git-cal + +=head2 OPTIONS + +=over + +=item [--period|-p]= + +Do not show the entire year: + +=over + +=item n = 1 to 12 + +Shows only one month (1=Jan .. 12=Dec) + +=item n = -11 to 0 + +Shows the previous -n months (and the current month) + +=back + +=item --author= + +View commits of a particular author. + +=item --all + +View stats from all branches. + +=item --ascii + +Display activity using ASCII characters instead of ANSI colors. + +=item --ansi + +Display activity using ANSI colors + +=item --unicode + +Display activity using unicode characters + +=item --help|-? + +Print this message. + +=back + +=head2 ADDITIONAL OPTIONS + + to view the logs of a particular file or directory + +=head2 USING GIT CONFIG + +git-cal uses the git config tool to store configuration on disk. Similar keys are used to +those listed above with the notable exception being the bundling of ascii, ansi and unicode +into a "format" key. Examples of the three supported keys are below. + + git config --global calendar.format ascii + + git config --global calendar.period 5 + + git config --global calendar.author karthik + +A command line supplied option will override the matching option set using this method. + +=head1 AUTHOR + +Karthik katooru + +=head1 COPYRIGHT AND LICENSE + +This program is free software; you can redistribute it and/or modify it under the MIT License diff --git a/bin/git-ignored b/bin/git-ignored new file mode 100755 index 0000000..4c4a1fa --- /dev/null +++ b/bin/git-ignored @@ -0,0 +1,6 @@ +#!/bin/sh +# +# List ignored files. +# +exec git ls-files --ignored --exclude-standard --others + diff --git a/bin/git-tree b/bin/git-tree new file mode 100755 index 0000000..58f05a7 --- /dev/null +++ b/bin/git-tree @@ -0,0 +1,10 @@ +#!/bin/sh +# +# Show a simplified tree with all branches. +# +exec git log \ + --all \ + --graph \ + --pretty='%C(auto)%h%d %s %C(8) %ad' \ + --simplify-by-decoration +