stagit-index.c (10356B)
1 #include <err.h> 2 #include <limits.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include <time.h> 7 #include <unistd.h> 8 9 #include <git2.h> 10 11 static git_repository *repo; 12 13 static const char *relpath = ""; 14 15 static char description[255] = "Repositories"; 16 static char *name = ""; 17 static char owner[255]; 18 19 static char * const default_style = 20 "body {\n" 21 " color: #000;\n" 22 " background-color: #fff;\n" 23 " font-family: monospace;\n" 24 "}\n" 25 "\n" 26 "h1, h2, h3, h4, h5, h6 {\n" 27 " font-size: 1em;\n" 28 " margin: 0;\n" 29 "}\n" 30 "\n" 31 "img, svg, h1, h2 {\n" 32 " vertical-align: middle;\n" 33 "}\n" 34 "\n" 35 "img {\n" 36 " border: 0;\n" 37 "}\n" 38 "\n" 39 "a:target {\n" 40 " background-color: #ccc;\n" 41 "}\n" 42 "\n" 43 "a.d,\n" 44 "a.h,\n" 45 "a.i,\n" 46 "a.line {\n" 47 " text-decoration: none;\n" 48 "}\n" 49 "\n" 50 "#blob a {\n" 51 " color: #555;\n" 52 "}\n" 53 " \n" 54 "#blob a:hover {\n" 55 " color: blue;\n" 56 " text-decoration: none;\n" 57 "}\n" 58 " \n" 59 "table thead td {\n" 60 " font-weight: bold;\n" 61 "}\n" 62 "\n" 63 "table td {\n" 64 " padding: 0 0.4em;\n" 65 "}\n" 66 "\n" 67 "#content table td {\n" 68 " vertical-align: top;\n" 69 " white-space: nowrap;\n" 70 "}\n" 71 " \n" 72 "#branches tr:hover td,\n" 73 "#tags tr:hover td,\n" 74 "#index tr:hover td,\n" 75 "#log tr:hover td,\n" 76 "#files tr:hover td {\n" 77 " background-color: #eee;\n" 78 "}\n" 79 " \n" 80 "#index tr td:nth-child(2),\n" 81 "#tags tr td:nth-child(3),\n" 82 "#branches tr td:nth-child(3),\n" 83 "#log tr td:nth-child(2) {\n" 84 " white-space: normal;\n" 85 "}\n" 86 " \n" 87 "td.num {\n" 88 " text-align: right;\n" 89 "}\n" 90 " \n" 91 ".desc {\n" 92 " color: #555;\n" 93 "}\n" 94 "\n" 95 "hr {\n" 96 " border: 0;\n" 97 " border-top: 1px solid #555;\n" 98 " height: 1px;\n" 99 "}\n" 100 "\n" 101 "pre {\n" 102 " font-family: monospace;\n" 103 "}\n" 104 "\n" 105 "pre a.h {\n" 106 " color: #00a;\n" 107 "}\n" 108 "\n" 109 ".A,\n" 110 "span.i,\n" 111 "pre a.i {\n" 112 " color: #070;\n" 113 "}\n" 114 "\n" 115 ".D,\n" 116 "span.d,\n" 117 "pre a.d {\n" 118 " color: #e00;\n" 119 "}\n" 120 "\n" 121 "pre a.h:hover,\n" 122 "pre a.i:hover,\n" 123 "pre a.d:hover {\n" 124 " text-decoration: none;\n" 125 "}\n" 126 "\n" 127 "@media (prefers-color-scheme: dark) {\n" 128 " body {\n" 129 " background-color: #000;\n" 130 " color: #bdbdbd;\n" 131 " }\n" 132 " hr {\n" 133 " border-color: #222;\n" 134 " }\n" 135 " a {\n" 136 " color: #56c8ff;\n" 137 " }\n" 138 " a:target {\n" 139 " background-color: #222;\n" 140 " }\n" 141 " .desc {\n" 142 " color: #aaa;\n" 143 " }\n" 144 " #blob a {\n" 145 " color: #555;\n" 146 " }\n" 147 " #blob a:target {\n" 148 " color: #eee;\n" 149 " }\n" 150 " #blob a:hover {\n" 151 " color: #56c8ff;\n" 152 " }\n" 153 " pre a.h {\n" 154 " color: #00cdcd;\n" 155 " }\n" 156 " .A,\n" 157 " span.i,\n" 158 " pre a.i {\n" 159 " color: #00cd00;\n" 160 " }\n" 161 " .D,\n" 162 " span.d,\n" 163 " pre a.d {\n" 164 " color: #cd0000;\n" 165 " }\n" 166 " #branches tr:hover td,\n" 167 " #tags tr:hover td,\n" 168 " #index tr:hover td,\n" 169 " #log tr:hover td,\n" 170 " #files tr:hover td {\n" 171 " background-color: #111;\n" 172 " }\n" 173 "}\n"; 174 175 static char *stylefile; 176 static char *style = default_style; 177 178 /* Handle read or write errors for a FILE * stream */ 179 void 180 checkfileerror(FILE *fp, const char *name, int mode) 181 { 182 if (mode == 'r' && ferror(fp)) 183 errx(1, "read error: %s", name); 184 else if (mode == 'w' && (fflush(fp) || ferror(fp))) 185 errx(1, "write error: %s", name); 186 } 187 188 void 189 joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) 190 { 191 int r; 192 193 r = snprintf(buf, bufsiz, "%s%s%s", 194 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 195 if (r < 0 || (size_t)r >= bufsiz) 196 errx(1, "path truncated: '%s%s%s'", 197 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 198 } 199 200 /* Percent-encode, see RFC3986 section 2.1. */ 201 void 202 percentencode(FILE *fp, const char *s, size_t len) 203 { 204 static char tab[] = "0123456789ABCDEF"; 205 unsigned char uc; 206 size_t i; 207 208 for (i = 0; *s && i < len; s++, i++) { 209 uc = *s; 210 /* NOTE: do not encode '/' for paths or ",-." */ 211 if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') || 212 uc == '[' || uc == ']') { 213 putc('%', fp); 214 putc(tab[(uc >> 4) & 0x0f], fp); 215 putc(tab[uc & 0x0f], fp); 216 } else { 217 putc(uc, fp); 218 } 219 } 220 } 221 222 /* Escape characters below as HTML 2.0 / XML 1.0. */ 223 void 224 xmlencode(FILE *fp, const char *s, size_t len) 225 { 226 size_t i; 227 228 for (i = 0; *s && i < len; s++, i++) { 229 switch(*s) { 230 case '<': fputs("<", fp); break; 231 case '>': fputs(">", fp); break; 232 case '\'': fputs("'" , fp); break; 233 case '&': fputs("&", fp); break; 234 case '"': fputs(""", fp); break; 235 default: putc(*s, fp); 236 } 237 } 238 } 239 240 void 241 printtimeshort(FILE *fp, const git_time *intime) 242 { 243 struct tm *intm; 244 time_t t; 245 char out[32]; 246 247 t = (time_t)intime->time; 248 if (!(intm = gmtime(&t))) 249 return; 250 strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); 251 fputs(out, fp); 252 } 253 254 void 255 writeheader(FILE *fp) 256 { 257 fputs("<!DOCTYPE html>\n" 258 "<html>\n<head>\n" 259 "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" 260 "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n" 261 "<title>", fp); 262 xmlencode(fp, description, strlen(description)); 263 fprintf(fp, 264 "</title>\n" 265 "<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/icons/apple-touch-icon.png\">\n" 266 "<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/icons/favicon-32x32.png\">\n" 267 "<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/icons/favicon-16x16.png\">\n" 268 "<link rel=\"manifest\" href=\"/icons/site.webmanifest\">\n" 269 "<link rel=\"mask-icon\" href=\"/icons/safari-pinned-tab.svg\" color=\"#ffb6c1\">\n" 270 "<link rel=\"shortcut icon\" href=\"/icons/favicon.ico\">\n" 271 "<meta name=\"msapplication-TileColor\" content=\"#603cba\">\n" 272 "<meta name=\"msapplication-config\" content=\"/icons/browserconfig.xml\">\n" 273 "<meta name=\"theme-color\" content=\"#ffffff\">\n"); 274 fputs("<style>\n", fp); 275 fputs(style, fp); 276 fputs("</style>\n", fp); 277 fputs("</head>\n<body>\n", fp); 278 fprintf(fp, "<table>\n<tr><td><a href=\"../\"><svg height=\"32px\" width=\"32px\" xmlns=\"http://www.w3.org/2000/svg\"><circle r=\"16\" cx=\"16\" cy=\"16\" fill=\"lightpink\"></circle></svg></a></td>\n" 279 "<td><span class=\"desc\">"); 280 xmlencode(fp, description, strlen(description)); 281 fputs("</span></td></tr><tr><td></td><td>\n" 282 "</td></tr>\n</table>\n<hr/>\n<div id=\"content\">\n" 283 "<table id=\"index\"><thead>\n" 284 "<tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Owner</b></td>" 285 "<td><b>Last commit</b></td></tr>" 286 "</thead><tbody>\n", fp); 287 } 288 289 void 290 writefooter(FILE *fp) 291 { 292 fputs("</tbody>\n</table>\n</div>\n</body>\n</html>\n", fp); 293 } 294 295 int 296 writelog(FILE *fp) 297 { 298 git_commit *commit = NULL; 299 const git_signature *author; 300 git_revwalk *w = NULL; 301 git_oid id; 302 char *stripped_name = NULL, *p; 303 int ret = 0; 304 305 git_revwalk_new(&w, repo); 306 git_revwalk_push_head(w); 307 308 if (git_revwalk_next(&id, w) || 309 git_commit_lookup(&commit, repo, &id)) { 310 ret = -1; 311 goto err; 312 } 313 314 author = git_commit_author(commit); 315 316 /* strip .git suffix */ 317 if (!(stripped_name = strdup(name))) 318 err(1, "strdup"); 319 if ((p = strrchr(stripped_name, '.'))) 320 if (!strcmp(p, ".git")) 321 *p = '\0'; 322 323 fputs("<tr><td><a href=\"", fp); 324 percentencode(fp, stripped_name, strlen(stripped_name)); 325 fputs("/log.html\">", fp); 326 xmlencode(fp, stripped_name, strlen(stripped_name)); 327 fputs("</a></td><td>", fp); 328 xmlencode(fp, description, strlen(description)); 329 fputs("</td><td>", fp); 330 xmlencode(fp, owner, strlen(owner)); 331 fputs("</td><td>", fp); 332 if (author) 333 printtimeshort(fp, &(author->when)); 334 fputs("</td></tr>", fp); 335 336 git_commit_free(commit); 337 err: 338 git_revwalk_free(w); 339 free(stripped_name); 340 341 return ret; 342 } 343 344 int 345 main(int argc, char *argv[]) 346 { 347 FILE *fp, *fpread; 348 char path[PATH_MAX], repodirabs[PATH_MAX + 1]; 349 const char *repodir; 350 int i, ret = 0; 351 352 if (argc < 2) { 353 fprintf(stderr, "usage: %s [-s stylefile] [repodir...]\n", argv[0]); 354 return 1; 355 } 356 357 /* do not search outside the git repository: 358 GIT_CONFIG_LEVEL_APP is the highest level currently */ 359 git_libgit2_init(); 360 for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) 361 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); 362 /* do not require the git repository to be owned by the current user */ 363 git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); 364 365 #ifdef __OpenBSD__ 366 if (pledge("stdio rpath", NULL) == -1) 367 err(1, "pledge"); 368 #endif 369 370 for (i = 1; i < argc; i++) { 371 if (argv[i][0] != '-') { 372 continue; 373 } 374 if (argv[i][1] == 's') { 375 if (i + 1 >= argc) 376 fprintf(stderr, "usage: %s [-s stylefile] [repodir...]\n", argv[0]); 377 stylefile = argv[++i]; 378 continue; 379 } 380 err(1, "usage: %s [-s stylefile] [repodir...]\n", argv[0]); 381 } 382 383 if (stylefile) { 384 if (!(fpread = fopen(stylefile, "r"))) 385 err(1, "fopen: '%s'", stylefile); 386 387 if (fpread) { 388 fseek(fpread, 0, SEEK_END); 389 long int len = ftell(fpread); 390 rewind(fpread); 391 if (len >= 0) { 392 style = calloc(1, (size_t)len + 1); 393 if (!fread(style, (size_t)len, 1, fpread)) { 394 free(style); 395 style = default_style; 396 } 397 } 398 checkfileerror(fpread, stylefile, 'r'); 399 fclose(fpread); 400 } 401 } 402 403 writeheader(stdout); 404 405 for (i = 1; i < argc; i++) { 406 if (argv[i][0] == '-') { 407 ++i; 408 continue; 409 } 410 411 repodir = argv[i]; 412 if (!realpath(repodir, repodirabs)) 413 err(1, "realpath"); 414 415 if (git_repository_open_ext(&repo, repodir, 416 GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) { 417 fprintf(stderr, "%s: cannot open repository\n", argv[0]); 418 ret = 1; 419 continue; 420 } 421 422 /* use directory name as name */ 423 if ((name = strrchr(repodirabs, '/'))) 424 name++; 425 else 426 name = ""; 427 428 /* read description or .git/description */ 429 joinpath(path, sizeof(path), repodir, "description"); 430 if (!(fp = fopen(path, "r"))) { 431 joinpath(path, sizeof(path), repodir, ".git/description"); 432 fp = fopen(path, "r"); 433 } 434 description[0] = '\0'; 435 if (fp) { 436 if (!fgets(description, sizeof(description), fp)) 437 description[0] = '\0'; 438 checkfileerror(fp, "description", 'r'); 439 fclose(fp); 440 } 441 442 /* read owner or .git/owner */ 443 joinpath(path, sizeof(path), repodir, "owner"); 444 if (!(fp = fopen(path, "r"))) { 445 joinpath(path, sizeof(path), repodir, ".git/owner"); 446 fp = fopen(path, "r"); 447 } 448 owner[0] = '\0'; 449 if (fp) { 450 if (!fgets(owner, sizeof(owner), fp)) 451 owner[0] = '\0'; 452 checkfileerror(fp, "owner", 'r'); 453 fclose(fp); 454 owner[strcspn(owner, "\n")] = '\0'; 455 } 456 writelog(stdout); 457 } 458 writefooter(stdout); 459 460 /* cleanup */ 461 git_repository_free(repo); 462 git_libgit2_shutdown(); 463 464 checkfileerror(stdout, "<stdout>", 'w'); 465 466 return ret; 467 }