gout

A static git page generator
git clone https://git.bracken.jp/gout.git
Log | Files | Refs | README | LICENSE

commit.c (8511B)


      1 #include "writer/gemini/commit.h"
      2 
      3 #include <assert.h>
      4 #include <err.h>
      5 #include <limits.h>
      6 #include <stdint.h>
      7 #include <stdio.h>
      8 #include <stdlib.h>
      9 #include <string.h>
     10 
     11 #include "format.h"
     12 #include "git/delta.h"
     13 #include "utils.h"
     14 #include "writer/gemini/page.h"
     15 
     16 struct GeminiCommit {
     17   FILE* out;
     18   const FileSystem* fs;
     19   GeminiPage* page;
     20   DiffLimits limits;
     21 };
     22 
     23 static void gemini_commit_write_summary(GeminiCommit* commit,
     24                                         const GitCommit* git_commit);
     25 static void gemini_commit_write_diffstat(GeminiCommit* commit,
     26                                          const GitCommit* git_commit);
     27 static void gemini_commit_write_diffstat_row(GeminiCommit* commit,
     28                                              const GitDelta* delta);
     29 static void gemini_commit_write_diff_content(GeminiCommit* commit,
     30                                              const GitCommit* git_commit);
     31 static void gemini_commit_write_diff_delta(GeminiCommit* commit,
     32                                            const GitDelta* delta);
     33 static void gemini_commit_write_diff_hunk(GeminiCommit* commit,
     34                                           const GitHunk* hunk);
     35 
     36 GeminiCommit* gemini_commit_create(const GitRepo* repo,
     37                                    const FileSystem* fs,
     38                                    const char* oid,
     39                                    const char* title) {
     40   assert(repo != NULL);
     41   assert(fs != NULL);
     42   assert(oid != NULL);
     43   GeminiCommit* commit = ecalloc(1, sizeof(GeminiCommit));
     44   commit->fs = fs;
     45   char filename[PATH_MAX];
     46   int r = snprintf(filename, sizeof(filename), "%s.gmi", oid);
     47   if (r < 0 || (size_t)r >= sizeof(filename)) {
     48     errx(1, "snprintf: filename truncated or error");
     49   }
     50   char* path = path_concat("commit", filename);
     51   commit->out = fs->fopen(path, "w");
     52   if (!commit->out) {
     53     err(1, "fopen: %s", path);
     54   }
     55   free(path);
     56   commit->page = gemini_page_create(commit->out, repo, fs, title, "../");
     57   commit->limits.max_files = 1000;
     58   commit->limits.max_deltas = 1000;
     59   commit->limits.max_delta_lines = 100000;
     60   return commit;
     61 }
     62 
     63 void gemini_commit_free(GeminiCommit* commit) {
     64   if (!commit) {
     65     return;
     66   }
     67   commit->fs->fclose(commit->out);
     68   gemini_page_free(commit->page);
     69   free(commit);
     70 }
     71 
     72 void gemini_commit_begin(GeminiCommit* commit) {
     73   assert(commit != NULL);
     74   gemini_page_begin(commit->page);
     75 }
     76 
     77 void gemini_commit_add_commit(GeminiCommit* commit,
     78                               const GitCommit* git_commit) {
     79   assert(commit != NULL);
     80   assert(git_commit != NULL);
     81   FILE* out = commit->out;
     82 
     83   gemini_commit_write_summary(commit, git_commit);
     84 
     85   size_t deltas_len = git_commit->deltas_len;
     86   if (deltas_len == 0) {
     87     return;
     88   }
     89   size_t addcount = git_commit->addcount;
     90   size_t delcount = git_commit->delcount;
     91   size_t filecount = git_commit->filecount;
     92   if (filecount > commit->limits.max_files ||
     93       deltas_len > commit->limits.max_deltas ||
     94       addcount > commit->limits.max_delta_lines ||
     95       delcount > commit->limits.max_delta_lines) {
     96     fprintf(out, "\nDiff is too large, output suppressed.\n");
     97     return;
     98   }
     99 
    100   gemini_commit_write_diffstat(commit, git_commit);
    101   gemini_commit_write_diff_content(commit, git_commit);
    102 }
    103 
    104 void gemini_commit_set_diff_limits(GeminiCommit* commit,
    105                                    const DiffLimits* limits) {
    106   assert(commit != NULL);
    107   assert(limits != NULL);
    108   commit->limits = *limits;
    109 }
    110 
    111 void gemini_commit_end(GeminiCommit* commit) {
    112   assert(commit != NULL);
    113   gemini_page_end(commit->page);
    114 }
    115 
    116 static void gemini_commit_write_summary(GeminiCommit* commit,
    117                                         const GitCommit* git_commit) {
    118   FILE* out = commit->out;
    119   const char* oid = git_commit->oid;
    120   fprintf(out, "=> ../commit/");
    121   print_percent_encoded(out, oid);
    122   fprintf(out, ".gmi commit %s\n", oid);
    123 
    124   const char* parentoid = git_commit->parentoid;
    125   if (parentoid && parentoid[0] != '\0') {
    126     fprintf(out, "=> ../commit/");
    127     print_percent_encoded(out, parentoid);
    128     fprintf(out, ".gmi parent %s\n", parentoid);
    129   }
    130 
    131   fprintf(out, "\n```\n");
    132   fprintf(out, "Author: %s <%s>\n", git_commit->author_name,
    133           git_commit->author_email);
    134   fprintf(out, "Date:   ");
    135   print_time(out, git_commit->author_time, git_commit->author_timezone_offset);
    136   fprintf(out, "\n");
    137 
    138   const char* message = git_commit->message;
    139   if (message) {
    140     fprintf(out, "\n%s\n", message);
    141   }
    142   fprintf(out, "```\n\n");
    143 }
    144 
    145 static void gemini_commit_write_diffstat(GeminiCommit* commit,
    146                                          const GitCommit* git_commit) {
    147   fprintf(commit->out, "### Diffstat\n\n```\n");
    148   size_t delta_count = git_commit->deltas_len;
    149   for (size_t i = 0; i < delta_count; i++) {
    150     gemini_commit_write_diffstat_row(commit, git_commit->deltas[i]);
    151   }
    152   fprintf(commit->out, "```\n\n");
    153 }
    154 
    155 static void gemini_commit_write_diffstat_row(GeminiCommit* commit,
    156                                              const GitDelta* delta) {
    157   static const size_t kGraphWidth = 30;
    158   FILE* out = commit->out;
    159 
    160   fprintf(out, "  %c ", delta->status);
    161   char filename[PATH_MAX];
    162   const char* old_file_path = delta->old_file_path;
    163   const char* new_file_path = delta->new_file_path;
    164   int r;
    165   if (strcmp(old_file_path, new_file_path) == 0) {
    166     r = snprintf(filename, sizeof(filename), "%s", old_file_path);
    167   } else {
    168     r = snprintf(filename, sizeof(filename), "%s -> %s", old_file_path,
    169                  new_file_path);
    170   }
    171   if (r < 0 || (size_t)r >= sizeof(filename)) {
    172     errx(1, "snprintf: filename truncated or error");
    173   }
    174   print_utf8_padded(out, filename, 35, ' ');
    175 
    176   size_t changed = delta->addcount + delta->delcount;
    177   fprintf(out, " | ");
    178   fprintf(out, "%7zu ", changed);
    179   char* added_graph = gitdelta_added_graph(delta, kGraphWidth);
    180   fprintf(out, "%s", added_graph);
    181   free(added_graph);
    182   char* deleted_graph = gitdelta_deleted_graph(delta, kGraphWidth);
    183   fprintf(out, "%s", deleted_graph);
    184   free(deleted_graph);
    185   fprintf(out, "\n");
    186 }
    187 
    188 static void gemini_commit_write_diff_content(GeminiCommit* commit,
    189                                              const GitCommit* git_commit) {
    190   FILE* out = commit->out;
    191   size_t addcount = git_commit->addcount;
    192   size_t delcount = git_commit->delcount;
    193   size_t filecount = git_commit->filecount;
    194   fprintf(out, "%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n\n",
    195           filecount, filecount == 1 ? "" : "s",  //
    196           addcount, addcount == 1 ? "" : "s",    //
    197           delcount, delcount == 1 ? "" : "s");
    198 
    199   size_t delta_count = git_commit->deltas_len;
    200   for (size_t i = 0; i < delta_count; i++) {
    201     gemini_commit_write_diff_delta(commit, git_commit->deltas[i]);
    202   }
    203 }
    204 
    205 static void gemini_commit_write_diff_delta(GeminiCommit* commit,
    206                                            const GitDelta* delta) {
    207   FILE* out = commit->out;
    208   fprintf(out, "=> ../file/");
    209   print_percent_encoded(out, delta->new_file_path);
    210   fprintf(out, ".gmi diff --git a/%s b/%s\n", delta->old_file_path,
    211           delta->new_file_path);
    212 
    213   if (delta->is_binary) {
    214     fprintf(out, "Binary files differ.\n\n");
    215   } else {
    216     fprintf(out, "```\n");
    217     size_t hunk_count = delta->hunks_len;
    218     for (size_t i = 0; i < hunk_count; i++) {
    219       gemini_commit_write_diff_hunk(commit, delta->hunks[i]);
    220     }
    221     fprintf(out, "```\n\n");
    222   }
    223 }
    224 
    225 static void gemini_commit_write_diff_hunk(GeminiCommit* commit,
    226                                           const GitHunk* hunk) {
    227   FILE* out = commit->out;
    228 
    229   // Output header. e.g. @@ -0,0 +1,3 @@
    230   fprintf(out, "%s\n", hunk->header);
    231 
    232   // Iterate over lines in hunk.
    233   size_t line_count = hunk->lines_len;
    234   for (size_t i = 0; i < line_count; i++) {
    235     const GitHunkLine* line = hunk->lines[i];
    236 
    237     const char* content = line->content;
    238     size_t content_len = line->content_len;
    239 
    240     // Strip trailing newline/CR from content.
    241     while (content_len > 0 && (content[content_len - 1] == '\n' ||
    242                                content[content_len - 1] == '\r')) {
    243       content_len--;
    244     }
    245 
    246     int old_lineno = line->old_lineno;
    247     int new_lineno = line->new_lineno;
    248     if (old_lineno == -1) {
    249       // Added line. Prefix with +.
    250       fprintf(out, "+%.*s\n", (int)content_len, content);
    251     } else if (new_lineno == -1) {
    252       // Removed line. Prefix with -.
    253       fprintf(out, "-%.*s\n", (int)content_len, content);
    254     } else {
    255       // Unchanged line. Prefix with ' '.
    256       fprintf(out, " %.*s\n", (int)content_len, content);
    257     }
    258   }
    259 }