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 }