git-stats

Command line utility showing plots of git commits over time in the terminal
git clone https://noulin.net/git/git-stats.git
Log | Files | Refs | README

git-stats.c (8843B)


      1 #! /usr/bin/env sheepy
      2 /* -./;
      3  * or direct path to sheepy: #! /usr/local/bin/sheepy */
      4 /* Libsheepy documentation: https://spartatek.se/libsheepy/ */
      5 #include "libsheepyObject.h"
      6 
      7 // git commands to get statistics:
      8 // https://devtut.github.io/git/git-statistics.html
      9 
     10 int argc; char **argv;
     11 
     12 /* enable/disable logging */
     13 /* #undef pLog */
     14 /* #define pLog(...) */
     15 
     16 // terminal size type
     17 typ struct {u16 rows; u16 cols;} winSizet;
     18 
     19 /* terminal window size */
     20 #include <sys/ioctl.h>
     21 #include <unistd.h>
     22 
     23 // get terminal window size
     24 winSizet wsize (void) {
     25     struct winsize w;
     26     ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
     27 
     28     return (winSizet){w.ws_row, w.ws_col};
     29 }
     30 
     31 time_t earliest, latest;
     32 char *firstDate = null, *lastDate = null;
     33 
     34 void drawGraph(char *from, char *gitopts, bool setTimeRange, bool barchart) {
     35 
     36   // Steps
     37   // get git statistics
     38   // sort the dates
     39   // remove time, keep only the date in a slice: slDates
     40   // count daily commits
     41   // compute min and max daily count
     42   // compute earliest and latest dates in unix time
     43   // get window size
     44   // aggregate daily count to window columns
     45   // create a blank display array
     46   // fill the display with the plot
     47   // show plot
     48 
     49   // get git statistics
     50   cleanListP(dates)   = execOutf("git log %s --pretty=format:\"%%ai\"", gitopts);
     51 
     52   // sort the dates
     53   sortG(&dates);
     54 
     55   char *ld = getG(dates, unusedV, -1);
     56   if (!ld) /*dates is empty, stop.*/ return;
     57   ld[4] = 0;
     58   if (parseInt(ld) < parseInt(from)) /* the last date is earlier than from, stop. */ return;
     59   ld[4] = '-';
     60 
     61   int cut      = 0;
     62   int fromYear = parseInt(from);
     63   enumerateS(dates, s, i) {
     64     s[4] = 0;
     65     int year = parseInt(s);
     66     s[4] = '-';
     67     if (year >= fromYear) {
     68       cut = i;
     69       break;
     70     }
     71   }
     72 
     73   if (cut) {
     74     delG(&dates, 0, cut);
     75   }
     76 
     77   if (lenG(dates) < 2) /* skip plot only 1 commit */ return;
     78 
     79   // remove time, keep only the date in a slice: slDates
     80   // count daily commits
     81   typ struct {
     82     char *date;
     83     int count;
     84   } dateCountst;
     85 
     86   sliceT(slDatest, dateCountst);
     87   slDatest slDates;
     88   sliceInit(&slDates);
     89 
     90   dateCountst d = {0};
     91   forEachS(dates, s) {
     92     s[11] = 0;
     93     if (not d.date) {
     94       // first element
     95       d.date = s;
     96       inc d.count;
     97     }
     98     else {
     99       if (eqG(d.date, s)) {
    100         inc d.count;
    101       }
    102       else {
    103         sliceAppend(&slDates, d);
    104         d.date  = s;
    105         d.count = 1;
    106       }
    107     }
    108   }
    109   // append last date
    110   sliceAppend(&slDates, d);
    111 
    112   // compute min and max daily count
    113   int minCount = sliceAt(&slDates, 0).count;
    114   int maxCount = 0;
    115 
    116   sliceForEach(&slDates, date) {
    117     minCount = MIN(date->count, minCount);
    118     maxCount = MAX(date->count, maxCount);
    119   }
    120 
    121   // compute earliest and latest dates in unix time
    122   if (setTimeRange) {
    123     earliest  = strToUnixTime(sliceFirst(&slDates).date, "%Y-%m-%d");
    124     latest    = strToUnixTime(sliceLast (&slDates).date, "%Y-%m-%d");
    125     firstDate = strdup(sliceFirst(&slDates).date);
    126     lastDate  = strdup(sliceLast (&slDates).date);
    127   }
    128 
    129   //logI("%"PRIu64" %"PRIu64" = %f", earliest, latest, (f64)((latest-earliest)/86400)/365);
    130 
    131   typ struct {
    132     int col;
    133     int value;
    134   } pet; // plot element type
    135 
    136   sliceT(plt, pet);
    137   plt slPl;
    138   sliceInit(&slPl);
    139 
    140   // get window size
    141   winSizet wz = wsize();
    142   #define graphHeight 20
    143 
    144 
    145   //logI("min %d, max %d, w %d h %d, first %s, last %s", minCount, maxCount, wz.cols, wz.rows, sliceFirst(&slDates).date, sliceLast(&slDates).date);
    146 
    147   // aggregate daily count to window columns
    148   pet p = {0};
    149 
    150   sliceForEach(&slDates, date) {
    151     time_t t = strToUnixTime(date->date, "%Y-%m-%d");
    152     int col;
    153     if (barchart)
    154       col  = (f64)(t-earliest)/(f64)(latest-earliest) * (f64)(wz.cols-1/*-1 to make sure it writes inside the display*/);
    155     else
    156       col  = (f64)(t-earliest)/(f64)(latest-earliest) * (f64)(wz.cols*2-1/*-1 to make sure it writes inside the display*/);
    157     if (col == p.col) {
    158       p.value = MAX(date->count, p.value);
    159     }
    160     else {
    161       sliceAppend(&slPl, p);
    162       p.col   = col;
    163       p.value = date->count;
    164     }
    165   }
    166   sliceAppend(&slPl, p);
    167 
    168   // create a blank display array
    169   // create a blank display array
    170   char *display[wz.cols][graphHeight];
    171 
    172   char *BARS[] = {" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"};
    173 
    174   u32 dotDisplay[wz.cols][graphHeight];
    175 
    176   if (barchart) {
    177     // clear display
    178     range(i, wz.cols) {
    179       range(j, graphHeight) {
    180         display[i][j] = BARS[0];
    181       }
    182     }
    183   }
    184   else {
    185     // clear dotDisplay
    186     range(i, wz.cols) {
    187       range(j, graphHeight) {
    188         dotDisplay[i][j] = ' ';
    189       }
    190     }
    191   }
    192 
    193   if (barchart) {
    194     // fill the display with the plot
    195     sliceForEach(&slPl, elm) {
    196       int dpHeight = (f64)(graphHeight*8) - (f64)elm->value/(f64)maxCount * (f64)(graphHeight*8);
    197       int bits     = dpHeight & 7;
    198       int cbar     = 8 - bits;
    199 
    200       // draw bar
    201       int h = dpHeight / 8;
    202 
    203       if (bits) {
    204         // print BARS[cbar]
    205         display[elm->col][h] = BARS[cbar];
    206         inc h;
    207       }
    208 
    209       rangeFrom(i, h, graphHeight) {
    210         display[elm->col][i] = BARS[8];
    211       }
    212 
    213       //logI("col %d, value %d", elm->col, elm->value);
    214       //logI("col %3d, value %4d %3d cbar %d bits %d", elm->col, dpHeight, h, cbar, bits);
    215     }
    216   }
    217   else {
    218     // for more information about braille, see:
    219     // https://en.wikipedia.org/wiki/Braille_Patterns
    220     const u32 brailleOffset = 0x2800;
    221     const u32 braille[4][2] = {
    222     {0x01, 0x08},
    223     {0x02, 0x10},
    224     {0x04, 0x20},
    225     {0x40, 0x80},
    226     };
    227 
    228     void setPoint(i32 x, i32 y) {
    229       dotDisplay[x/2][y/4] |= braille[y%4][x%2] + brailleOffset;
    230     }
    231 
    232     void line(i32 x0, i32 y0, i32 x1, i32 y1) {
    233       // bresenham's line algorithm
    234       // https://en.wikipedia.org/wiki/Bresenham's_line_algorithm
    235       i32 dx = abs(x1-x0);
    236       i32 sx = x0<x1 ? 1 : -1;
    237       i32 dy = -abs(y1-y0);
    238       i32 sy = y0<y1 ? 1 : -1;
    239       // error value e_xy
    240       i32 err = dx+dy;
    241       loopBreakerInit;
    242       forever {
    243         loopBreaker(10000);
    244         setPoint(x0, y0);
    245         if (x0 == x1 and y0 == y1) break;
    246         i32 e2 = 2*err;
    247         if (e2 >= dy) {
    248           // e_xy+e_x > 0
    249           err += dy;
    250           x0  += sx;
    251         }
    252         if (e2 <= dx) {
    253           // e_xy+e_y < 0
    254           err += dx;
    255           y0  += sy;
    256         }
    257       }
    258     }
    259 
    260 
    261     // fill the display with the plot
    262     sliceEnumerate(&slPl, sli, elm) {
    263       if (sli > 0) {
    264         int dpHeight   = (f64)(graphHeight*4) - (f64)elm->value/(f64)maxCount * (f64)(graphHeight*4);
    265         int prevHeight = (f64)(graphHeight*4) - (f64)sliceAt(&slPl,sli-1).value/(f64)maxCount * (f64)(graphHeight*4);
    266         line(sliceAt(&slPl,sli-1).col, prevHeight, elm->col, dpHeight);
    267       }
    268       //logI("col %d, value %d", elm->col, elm->value);
    269     }
    270   }
    271 
    272   // show plot
    273   char maxC[20] = {0};
    274   snprintf(maxC, sizeof(maxC), "%d", maxCount);
    275   var ln = strlen(maxC);
    276   snprintf(maxC, sizeof(maxC), BLD"%d"RST, maxCount);
    277   #define HORIZONTAL_DASH "┈"
    278   printf(maxC);
    279   rangeFrom(i, ln, wz.cols) {
    280     printf(HORIZONTAL_DASH);
    281   }
    282   put;
    283   if (barchart) {
    284     range(j, graphHeight) {
    285       range(i, wz.cols) {
    286         printf(display[i][j]);
    287       }
    288       put;
    289     }
    290   }
    291   else {
    292     range(j, graphHeight) {
    293       range(i, wz.cols) {
    294         char dots[10] = {0};
    295         bRune2CodeUTF8(dots, dotDisplay[i][j]);
    296         printf(BLD"%s"RST, dots);
    297       }
    298       put;
    299     }
    300   }
    301   range(i, wz.cols) {
    302     printf(HORIZONTAL_DASH);
    303   }
    304   put;
    305   int col         = strlen(firstDate);
    306   printf(BLD"%s"RST, firstDate);
    307   time_t halfTime = (earliest+latest)/2;
    308   char hT[5];
    309   bLTimeToYMDS(hT, sizeof(hT), halfTime);
    310   int spaces      = (wz.cols/2-2)-col;
    311   col            += spaces + 4 /*halfTime string length*/;
    312   range(i, spaces) printf(" ");
    313   printf(BLD"%s"RST, hT);
    314   spaces = wz.cols - strlen(lastDate) - col;
    315   range(i, spaces) printf(" ");
    316   printf(BLD"%s"RST, lastDate);
    317 
    318 
    319   sliceFree(&slPl);
    320   sliceFree(&slDates);
    321 }
    322 
    323 int main(int ARGC, char** ARGV) {
    324 
    325   argc = ARGC; argv = ARGV;
    326 
    327   initLibsheepy(ARGV[0]);
    328   setLogMode(LOG_DATE);
    329   //openProgLogFile();
    330   //setLogSymbols(LOG_UTF8);
    331   //disableLibsheepyErrorLogs;
    332 
    333 
    334   char *from    = null;
    335   bool barchart = no;
    336   if (argc > 1) {
    337     if (isInt(argv[1])) {
    338       from     = argv[1];
    339       barchart = argv[2] and eqG(argv[2], "bar");
    340     }
    341     else
    342       barchart = argv[1] and eqG(argv[1], "bar");
    343   }
    344 
    345   drawGraph(from, "", /*setTimeRange*/ yes, barchart);
    346 
    347   // get list of authors
    348   // git shortlog -sn
    349   cleanListP(authors) = execOut("git shortlog -sn");
    350 
    351   // get commit dates for each author
    352   // git log --author="Remy" --pretty=format:"%ai"
    353   forEachS(authors, l) {
    354     puts(&l[7]);
    355     cleanCharP(auth) = formatS("--author=\"%s\"", &l[7]);
    356     drawGraph(from, auth, /*setTimeRange*/ no, barchart);
    357   }
    358 
    359   freeManyS(firstDate, lastDate);
    360 }
    361 // vim: set expandtab ts=2 sw=2: