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 }