commit.c (8636B)
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_path = delta->old_file_path ? delta->old_file_path : ""; 163 const char* new_path = delta->new_file_path ? delta->new_file_path : ""; 164 int r; 165 if (strcmp(old_path, new_path) == 0) { 166 r = snprintf(filename, sizeof(filename), "%s", old_path); 167 } else { 168 r = snprintf(filename, sizeof(filename), "%s -> %s", old_path, 169 new_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 const char* old_path = delta->old_file_path ? delta->old_file_path : ""; 209 const char* new_path = delta->new_file_path ? delta->new_file_path : ""; 210 fprintf(out, "=> ../file/"); 211 print_percent_encoded(out, new_path); 212 fprintf(out, ".gmi diff --git a/%s b/%s\n", old_path, new_path); 213 214 if (delta->is_binary) { 215 fprintf(out, "Binary files differ.\n\n"); 216 } else { 217 fprintf(out, "```\n"); 218 size_t hunk_count = delta->hunks_len; 219 for (size_t i = 0; i < hunk_count; i++) { 220 gemini_commit_write_diff_hunk(commit, delta->hunks[i]); 221 } 222 fprintf(out, "```\n\n"); 223 } 224 } 225 226 static void gemini_commit_write_diff_hunk(GeminiCommit* commit, 227 const GitHunk* hunk) { 228 FILE* out = commit->out; 229 230 // Output header. e.g. @@ -0,0 +1,3 @@ 231 fprintf(out, "%s\n", hunk->header); 232 233 // Iterate over lines in hunk. 234 size_t line_count = hunk->lines_len; 235 for (size_t i = 0; i < line_count; i++) { 236 const GitHunkLine* line = hunk->lines[i]; 237 238 const char* content = line->content; 239 size_t content_len = line->content_len; 240 241 // Strip trailing newline/CR from content. 242 while (content_len > 0 && (content[content_len - 1] == '\n' || 243 content[content_len - 1] == '\r')) { 244 content_len--; 245 } 246 247 int old_lineno = line->old_lineno; 248 int new_lineno = line->new_lineno; 249 if (old_lineno == -1) { 250 // Added line. Prefix with +. 251 fprintf(out, "+%.*s\n", (int)content_len, content); 252 } else if (new_lineno == -1) { 253 // Removed line. Prefix with -. 254 fprintf(out, "-%.*s\n", (int)content_len, content); 255 } else { 256 // Unchanged line. Prefix with ' '. 257 fprintf(out, " %.*s\n", (int)content_len, content); 258 } 259 } 260 }