gout

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

commit.c (8577B)


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