stagit-index.c (8202B)
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 /* Handle read or write errors for a FILE * stream */ 20 void 21 checkfileerror(FILE *fp, const char *name, int mode) 22 { 23 if (mode == 'r' && ferror(fp)) 24 errx(1, "read error: %s", name); 25 else if (mode == 'w' && (fflush(fp) || ferror(fp))) 26 errx(1, "write error: %s", name); 27 } 28 29 void 30 joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) 31 { 32 int r; 33 34 r = snprintf(buf, bufsiz, "%s%s%s", 35 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 36 if (r < 0 || (size_t)r >= bufsiz) 37 errx(1, "path truncated: '%s%s%s'", 38 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 39 } 40 41 /* Percent-encode, see RFC3986 section 2.1. */ 42 void 43 percentencode(FILE *fp, const char *s, size_t len) 44 { 45 static char tab[] = "0123456789ABCDEF"; 46 unsigned char uc; 47 size_t i; 48 49 for (i = 0; *s && i < len; s++, i++) { 50 uc = *s; 51 /* NOTE: do not encode '/' for paths or ",-." */ 52 if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') || 53 uc == '[' || uc == ']') { 54 putc('%', fp); 55 putc(tab[(uc >> 4) & 0x0f], fp); 56 putc(tab[uc & 0x0f], fp); 57 } else { 58 putc(uc, fp); 59 } 60 } 61 } 62 63 /* Escape characters below as HTML 2.0 / XML 1.0. */ 64 void 65 xmlencode(FILE *fp, const char *s, size_t len) 66 { 67 size_t i; 68 69 for (i = 0; *s && i < len; s++, i++) { 70 switch(*s) { 71 case '<': fputs("<", fp); break; 72 case '>': fputs(">", fp); break; 73 case '\'': fputs("'" , fp); break; 74 case '&': fputs("&", fp); break; 75 case '"': fputs(""", fp); break; 76 default: putc(*s, fp); 77 } 78 } 79 } 80 81 void 82 printtimeshort(FILE *fp, const git_time *intime) 83 { 84 struct tm *intm; 85 time_t t; 86 char out[32]; 87 88 t = (time_t)intime->time; 89 if (!(intm = gmtime(&t))) 90 return; 91 strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); 92 fputs(out, fp); 93 } 94 95 void 96 writejavascript(FILE *fp) 97 { 98 fputs( 99 "<script>\n" 100 "(function() {\n" 101 " const toggle = document.getElementById('theme-toggle');\n" 102 " const icon = document.getElementById('theme-icon');\n" 103 " const label = document.getElementById('theme-label');\n" 104 " const body = document.body;\n" 105 "\n" 106 " function setTheme(theme) {\n" 107 " if (theme === 'dark') {\n" 108 " body.classList.add('dark-mode');\n" 109 " icon.textContent = '☀️';\n" 110 " label.textContent = 'Light';\n" 111 " } else {\n" 112 " body.classList.remove('dark-mode');\n" 113 " icon.textContent = '🌙';\n" 114 " label.textContent = 'Dark';\n" 115 " }\n" 116 " localStorage.setItem('theme', theme);\n" 117 " }\n" 118 "\n" 119 " // Check saved preference\n" 120 " const saved = localStorage.getItem('theme');\n" 121 " if (saved) {\n" 122 " setTheme(saved);\n" 123 " } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {\n" 124 " setTheme('dark');\n" 125 " }\n" 126 "\n" 127 " toggle.addEventListener('click', function() {\n" 128 " const isDark = body.classList.contains('dark-mode');\n" 129 " setTheme(isDark ? 'light' : 'dark');\n" 130 " });\n" 131 "})();\n" 132 "</script>\n", 133 fp 134 ); 135 } 136 137 void 138 writeheader(FILE *fp) 139 { 140 fputs("<!DOCTYPE html>\n" 141 "<html>\n<head>\n" 142 "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" 143 "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n" 144 "<title>", fp); 145 xmlencode(fp, description, strlen(description)); 146 fprintf(fp, "</title>\n<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath); 147 fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath); 148 fputs("</head>\n<body>\n", fp); 149 fputs("<table style=\"width: 100%;\">\n", fp); 150 fputs("<tr>\n", fp); 151 fputs(" <td style=\"width: 32px; vertical-align: middle;\">\n", fp); 152 fprintf(fp, " <img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" />\n", relpath); 153 fputs(" </td>\n", fp); 154 fputs(" <td style=\"vertical-align: middle;\">\n", fp); 155 fputs(" <span style=\"display: block; font-size: 1em; margin: 0;\">", fp); 156 xmlencode(fp, description, strlen(description)); 157 fputs("</span>\n", fp); 158 fputs(" </td>\n", fp); 159 fputs(" <td style=\"text-align: right; vertical-align: top; padding-top: 4px; white-space: nowrap;\">\n", fp); 160 fputs(" <button id=\"theme-toggle\" style=\"background: none; border: 1px solid #888; border-radius: 4px; padding: 4px 8px; cursor: pointer;\">\n", fp); 161 fputs(" <span id=\"theme-icon\">🌙</span> <span id=\"theme-label\">Dark</span>\n", fp); 162 fputs(" </button>\n", fp); 163 fputs(" </td>\n", fp); 164 fputs("</tr>\n", fp); 165 fputs("</table>\n<hr/>\n<div id=\"content\">\n" 166 "<table id=\"index\"><thead>\n" 167 "<tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Owner</b></td>" 168 "<td><b>Last commit</b></td></tr>" 169 "</thead><tbody>\n", fp); 170 writejavascript(fp); 171 } 172 173 void 174 writefooter(FILE *fp) 175 { 176 fputs("</tbody>\n</table>\n</div>\n</body>\n</html>\n", fp); 177 } 178 179 int 180 writelog(FILE *fp) 181 { 182 git_commit *commit = NULL; 183 const git_signature *author; 184 git_revwalk *w = NULL; 185 git_oid id; 186 char *stripped_name = NULL, *p; 187 int ret = 0; 188 189 git_revwalk_new(&w, repo); 190 git_revwalk_push_head(w); 191 192 if (git_revwalk_next(&id, w) || 193 git_commit_lookup(&commit, repo, &id)) { 194 ret = -1; 195 goto err; 196 } 197 198 author = git_commit_author(commit); 199 200 /* strip .git suffix */ 201 if (!(stripped_name = strdup(name))) 202 err(1, "strdup"); 203 if ((p = strrchr(stripped_name, '.'))) 204 if (!strcmp(p, ".git")) 205 *p = '\0'; 206 207 fputs("<tr><td><a href=\"", fp); 208 percentencode(fp, stripped_name, strlen(stripped_name)); 209 fputs("/log.html\">", fp); 210 xmlencode(fp, stripped_name, strlen(stripped_name)); 211 fputs("</a></td><td>", fp); 212 xmlencode(fp, description, strlen(description)); 213 fputs("</td><td>", fp); 214 xmlencode(fp, owner, strlen(owner)); 215 fputs("</td><td>", fp); 216 if (author) 217 printtimeshort(fp, &(author->when)); 218 fputs("</td></tr>", fp); 219 220 git_commit_free(commit); 221 err: 222 git_revwalk_free(w); 223 free(stripped_name); 224 225 return ret; 226 } 227 228 int 229 main(int argc, char *argv[]) 230 { 231 FILE *fp; 232 char path[PATH_MAX], repodirabs[PATH_MAX + 1]; 233 const char *repodir; 234 int i, ret = 0; 235 236 if (argc < 2) { 237 fprintf(stderr, "usage: %s [repodir...]\n", argv[0]); 238 return 1; 239 } 240 241 /* do not search outside the git repository: 242 GIT_CONFIG_LEVEL_APP is the highest level currently */ 243 git_libgit2_init(); 244 for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) 245 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); 246 /* do not require the git repository to be owned by the current user */ 247 git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); 248 249 #ifdef __OpenBSD__ 250 if (pledge("stdio rpath", NULL) == -1) 251 err(1, "pledge"); 252 #endif 253 254 writeheader(stdout); 255 256 for (i = 1; i < argc; i++) { 257 repodir = argv[i]; 258 if (!realpath(repodir, repodirabs)) 259 err(1, "realpath"); 260 261 if (git_repository_open_ext(&repo, repodir, 262 GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) { 263 fprintf(stderr, "%s: cannot open repository\n", argv[0]); 264 ret = 1; 265 continue; 266 } 267 268 /* use directory name as name */ 269 if ((name = strrchr(repodirabs, '/'))) 270 name++; 271 else 272 name = ""; 273 274 /* read description or .git/description */ 275 joinpath(path, sizeof(path), repodir, "description"); 276 if (!(fp = fopen(path, "r"))) { 277 joinpath(path, sizeof(path), repodir, ".git/description"); 278 fp = fopen(path, "r"); 279 } 280 description[0] = '\0'; 281 if (fp) { 282 if (!fgets(description, sizeof(description), fp)) 283 description[0] = '\0'; 284 checkfileerror(fp, "description", 'r'); 285 fclose(fp); 286 } 287 288 /* read owner or .git/owner */ 289 joinpath(path, sizeof(path), repodir, "owner"); 290 if (!(fp = fopen(path, "r"))) { 291 joinpath(path, sizeof(path), repodir, ".git/owner"); 292 fp = fopen(path, "r"); 293 } 294 owner[0] = '\0'; 295 if (fp) { 296 if (!fgets(owner, sizeof(owner), fp)) 297 owner[0] = '\0'; 298 checkfileerror(fp, "owner", 'r'); 299 fclose(fp); 300 owner[strcspn(owner, "\n")] = '\0'; 301 } 302 writelog(stdout); 303 } 304 writefooter(stdout); 305 306 /* cleanup */ 307 git_repository_free(repo); 308 git_libgit2_shutdown(); 309 310 checkfileerror(stdout, "<stdout>", 'w'); 311 312 return ret; 313 }