commit.c (10236B)
1 #include "writer/html/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/html/page.h" 16 17 struct HtmlCommit { 18 FILE* out; 19 const FileSystem* fs; 20 HtmlPage* page; 21 DiffLimits limits; 22 }; 23 24 static void html_commit_write_summary(HtmlCommit* commit, 25 const GitCommit* git_commit); 26 static void html_commit_write_diffstat(HtmlCommit* commit, 27 const GitCommit* git_commit); 28 static void html_commit_write_diffstat_row(HtmlCommit* commit, 29 size_t row, 30 const GitDelta* delta); 31 static void html_commit_write_diff_content(HtmlCommit* commit, 32 const GitCommit* git_commit); 33 static void html_commit_write_diff_delta(HtmlCommit* commit, 34 size_t file_num, 35 const GitDelta* delta); 36 static void html_commit_write_diff_hunk(HtmlCommit* commit, 37 size_t file_num, 38 const GitHunk* hunk); 39 40 HtmlCommit* html_commit_create(const GitRepo* repo, 41 const FileSystem* fs, 42 const char* oid, 43 const char* title) { 44 assert(repo != NULL); 45 assert(fs != NULL); 46 assert(oid != NULL); 47 HtmlCommit* commit = ecalloc(1, sizeof(HtmlCommit)); 48 commit->fs = fs; 49 char filename[PATH_MAX]; 50 int r = snprintf(filename, sizeof(filename), "%s.html", oid); 51 if (r < 0 || (size_t)r >= sizeof(filename)) { 52 errx(1, "snprintf: filename truncated or error"); 53 } 54 char* path = path_concat("commit", filename); 55 commit->out = fs->fopen(path, "w"); 56 if (!commit->out) { 57 err(1, "fopen: %s", path); 58 } 59 free(path); 60 commit->page = html_page_create(commit->out, repo, fs, title, "../"); 61 commit->limits.max_files = 1000; 62 commit->limits.max_deltas = 1000; 63 commit->limits.max_delta_lines = 100000; 64 return commit; 65 } 66 67 void html_commit_free(HtmlCommit* commit) { 68 if (!commit) { 69 return; 70 } 71 commit->fs->fclose(commit->out); 72 commit->out = NULL; 73 html_page_free(commit->page); 74 commit->page = NULL; 75 free(commit); 76 } 77 78 void html_commit_begin(HtmlCommit* commit) { 79 assert(commit != NULL); 80 html_page_begin(commit->page); 81 } 82 83 void html_commit_add_commit(HtmlCommit* commit, const GitCommit* git_commit) { 84 assert(commit != NULL); 85 assert(git_commit != NULL); 86 FILE* out = commit->out; 87 88 html_commit_write_summary(commit, git_commit); 89 90 size_t deltas_len = git_commit->deltas_len; 91 if (deltas_len == 0) { 92 return; 93 } 94 size_t addcount = git_commit->addcount; 95 size_t delcount = git_commit->delcount; 96 size_t filecount = git_commit->filecount; 97 if (filecount > commit->limits.max_files || 98 deltas_len > commit->limits.max_deltas || 99 addcount > commit->limits.max_delta_lines || 100 delcount > commit->limits.max_delta_lines) { 101 fprintf(out, "<pre>Diff is too large, output suppressed.</pre>\n"); 102 return; 103 } 104 105 html_commit_write_diffstat(commit, git_commit); 106 html_commit_write_diff_content(commit, git_commit); 107 } 108 109 void html_commit_set_diff_limits(HtmlCommit* commit, const DiffLimits* limits) { 110 assert(commit != NULL); 111 assert(limits != NULL); 112 commit->limits = *limits; 113 } 114 115 void html_commit_end(HtmlCommit* commit) { 116 assert(commit != NULL); 117 html_page_end(commit->page); 118 } 119 120 static void html_commit_write_summary(HtmlCommit* commit, 121 const GitCommit* git_commit) { 122 FILE* out = commit->out; 123 const char* oid = git_commit->oid; 124 fprintf(out, "<pre><b>commit</b> "); 125 fprintf(out, "<a href=\"../commit/%s.html\">%s</a>\n", oid, oid); 126 127 const char* parentoid = git_commit->parentoid; 128 if (parentoid && parentoid[0] != '\0') { 129 fprintf(out, "<b>parent</b> "); 130 fprintf(out, "<a href=\"../commit/%s.html\">%s</a>\n", parentoid, 131 parentoid); 132 } 133 134 fprintf(out, "<b>Author:</b> "); 135 print_xml_encoded(out, git_commit->author_name); 136 const char* email = git_commit->author_email; 137 if (is_safe_mailto(email)) { 138 fprintf(out, " <<a href=\"mailto:"); 139 print_xml_encoded(out, email); 140 fprintf(out, "\">"); 141 print_xml_encoded(out, email); 142 fprintf(out, "</a>>\n"); 143 } else { 144 fprintf(out, " <"); 145 print_xml_encoded(out, email); 146 fprintf(out, ">\n"); 147 } 148 fprintf(out, "<b>Date:</b> "); 149 print_time(out, git_commit->author_time, git_commit->author_timezone_offset); 150 fprintf(out, "\n"); 151 152 const char* message = git_commit->message; 153 if (message) { 154 fprintf(out, "\n"); 155 print_xml_encoded(out, message); 156 fprintf(out, "\n"); 157 } 158 } 159 160 static void html_commit_write_diffstat(HtmlCommit* commit, 161 const GitCommit* git_commit) { 162 fprintf(commit->out, "<b>Diffstat:</b>\n<table>"); 163 size_t delta_count = git_commit->deltas_len; 164 for (size_t i = 0; i < delta_count; i++) { 165 html_commit_write_diffstat_row(commit, i, git_commit->deltas[i]); 166 } 167 fprintf(commit->out, "</table></pre>"); 168 } 169 170 static void html_commit_write_diffstat_row(HtmlCommit* commit, 171 size_t row, 172 const GitDelta* delta) { 173 static const size_t kGraphWidth = 78; 174 FILE* out = commit->out; 175 176 char status = delta->status; 177 if (status == ' ') { 178 fprintf(out, "<tr><td>"); 179 } else { 180 fprintf(out, "<tr><td class=\"%c\">%c", status, status); 181 } 182 fprintf(out, "</td>"); 183 fprintf(out, "<td>"); 184 fprintf(out, "<a href=\"#h%zu\">", row); 185 const char* old_file_path = delta->old_file_path; 186 const char* new_file_path = delta->new_file_path; 187 print_xml_encoded(out, old_file_path); 188 if (strcmp(old_file_path, new_file_path) != 0) { 189 fprintf(out, " -> "); 190 print_xml_encoded(out, new_file_path); 191 } 192 fprintf(out, "</a></td>"); 193 194 size_t changed = delta->addcount + delta->delcount; 195 fprintf(out, "<td> | </td>"); 196 fprintf(out, "<td class=\"num\">%zu</td>", changed); 197 char* added_graph = gitdelta_added_graph(delta, kGraphWidth); 198 fprintf(out, "<td><span class=\"i\">%s</span>", added_graph); 199 free(added_graph); 200 char* deleted_graph = gitdelta_deleted_graph(delta, kGraphWidth); 201 fprintf(out, "<span class=\"d\">%s</span>", deleted_graph); 202 free(deleted_graph); 203 fprintf(out, "</td></tr>\n"); 204 } 205 206 static void html_commit_write_diff_content(HtmlCommit* commit, 207 const GitCommit* git_commit) { 208 FILE* out = commit->out; 209 size_t filecount = git_commit->filecount; 210 size_t addcount = git_commit->addcount; 211 size_t delcount = git_commit->delcount; 212 fprintf(out, "<pre>"); 213 fprintf(out, "%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n", 214 filecount, filecount == 1 ? "" : "s", // 215 addcount, addcount == 1 ? "" : "s", // 216 delcount, delcount == 1 ? "" : "s"); 217 fprintf(out, "<hr/>"); 218 219 size_t delta_count = git_commit->deltas_len; 220 for (size_t i = 0; i < delta_count; i++) { 221 html_commit_write_diff_delta(commit, i, git_commit->deltas[i]); 222 } 223 fprintf(out, "</pre>\n"); 224 } 225 226 static void html_commit_write_diff_delta(HtmlCommit* commit, 227 size_t file_num, 228 const GitDelta* delta) { 229 const char* old_file_path = delta->old_file_path; 230 const char* new_file_path = delta->new_file_path; 231 fprintf(commit->out, "<b>diff --git "); 232 fprintf(commit->out, "a/<a id=\"h%zu\" href=\"../file/", file_num); 233 print_percent_encoded(commit->out, old_file_path); 234 fprintf(commit->out, ".html\">"); 235 print_xml_encoded(commit->out, old_file_path); 236 fprintf(commit->out, "</a> b/<a href=\"../file/"); 237 print_percent_encoded(commit->out, new_file_path); 238 fprintf(commit->out, ".html\">"); 239 print_xml_encoded(commit->out, new_file_path); 240 fprintf(commit->out, "</a></b>\n"); 241 242 if (delta->is_binary) { 243 fprintf(commit->out, "Binary files differ.\n"); 244 } else { 245 size_t hunk_count = delta->hunks_len; 246 for (size_t i = 0; i < hunk_count; i++) { 247 html_commit_write_diff_hunk(commit, file_num, delta->hunks[i]); 248 } 249 } 250 } 251 252 static void html_commit_write_diff_hunk(HtmlCommit* commit, 253 size_t file_num, 254 const GitHunk* hunk) { 255 FILE* out = commit->out; 256 257 // Output header. e.g. @@ -0,0 +1,3 @@ 258 char hdr_id[32]; 259 size_t hunkid = hunk->id; 260 int r = snprintf(hdr_id, sizeof(hdr_id), "h%zu-%zu", file_num, hunkid); 261 if (r < 0 || (size_t)r >= sizeof(hdr_id)) { 262 errx(1, "snprintf: hunk ID truncated"); 263 } 264 fprintf(out, "<a href=\"#%s\" id=\"%s\" class=\"h\">", hdr_id, hdr_id); 265 const char* header = hunk->header; 266 print_xml_encoded(out, header); 267 fprintf(out, "</a>"); 268 269 // Iterate over lines in hunk. 270 size_t line_count = hunk->lines_len; 271 for (size_t i = 0; i < line_count; i++) { 272 const GitHunkLine* line = hunk->lines[i]; 273 size_t hunklineid = line->id; 274 char line_id[64]; 275 int r = snprintf(line_id, sizeof(line_id), "%s-%zu", hdr_id, hunklineid); 276 if (r < 0 || (size_t)r >= sizeof(line_id)) { 277 errx(1, "snprintf: line ID truncated"); 278 } 279 const char* content = line->content; 280 size_t content_len = line->content_len; 281 int old_lineno = line->old_lineno; 282 int new_lineno = line->new_lineno; 283 if (old_lineno == -1) { 284 // Added line. Prefix with +. 285 fprintf(out, "<a href=\"#%s\" id=\"%s\" class=\"i\">+", line_id, line_id); 286 print_xml_encoded_len(out, content, content_len, false); 287 fprintf(out, "\n</a>"); 288 } else if (new_lineno == -1) { 289 // Removed line. Prefix with -. 290 fprintf(out, "<a href=\"#%s\" id=\"%s\" class=\"d\">-", line_id, line_id); 291 print_xml_encoded_len(out, content, content_len, false); 292 fprintf(out, "\n</a>"); 293 } else { 294 // Unchanged line. Prefix with ' '. No link. 295 fprintf(out, " "); 296 print_xml_encoded_len(out, content, content_len, false); 297 fprintf(out, "\n"); 298 } 299 } 300 }