stagit~ Unnamed repository; edit this file 'description' to name the repository. |
| log | files | refs | archive | README | LICENSE |
stagit-index.c (8825B)
1 #include <err.h> 2 #include <git2.h> 3 #include <limits.h> 4 #include <stdio.h> 5 #include <stdlib.h> 6 #include <string.h> 7 #include <time.h> 8 #include <unistd.h> 9 10 struct repoinfo { 11 char path[PATH_MAX + 1]; 12 char name[PATH_MAX + 1]; 13 char description[255]; 14 time_t last_commit_time; 15 }; 16 17 static git_repository *repo; 18 static const char *relpath = ""; 19 static char description[255] = "evil.djnn.sh"; 20 static char *name = ""; 21 static char category[255]; 22 23 time_t get_last_commit_time(const char *repodir) { 24 git_repository *repo = NULL; 25 git_revwalk *w = NULL; 26 git_commit *commit = NULL; 27 git_oid id; 28 time_t t = 0; 29 30 if (git_repository_open_ext(&repo, repodir, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) 31 return 0; 32 if (git_revwalk_new(&w, repo) == 0) { 33 git_revwalk_push_head(w); 34 if (git_revwalk_next(&id, w) == 0) { 35 if (git_commit_lookup(&commit, repo, &id) == 0) { 36 const git_signature *author = git_commit_author(commit); 37 if (author) 38 t = author->when.time; 39 git_commit_free(commit); 40 } 41 } 42 git_revwalk_free(w); 43 } 44 git_repository_free(repo); 45 return t; 46 } 47 48 /* Handle read or write errors for a FILE * stream */ 49 void checkfileerror(FILE *fp, const char *name, int mode) { 50 if (mode == 'r' && ferror(fp)) 51 errx(1, "read error: %s", name); 52 else if (mode == 'w' && (fflush(fp) || ferror(fp))) 53 errx(1, "write error: %s", name); 54 } 55 56 void joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) { 57 int r; 58 r = snprintf(buf, bufsiz, "%s%s%s", path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 59 if (r < 0 || (size_t)r >= bufsiz) 60 errx(1, "path truncated: '%s%s%s'", path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 61 } 62 63 /* Percent-encode, see RFC3986 section 2.1. */ 64 void percentencode(FILE *fp, const char *s, size_t len) { 65 static char tab[] = "0123456789ABCDEF"; 66 unsigned char uc; 67 size_t i; 68 for (i = 0; *s && i < len; s++, i++) { 69 uc = *s; 70 /* NOTE: do not encode '/' for paths or ",-." */ 71 if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') || uc == '[' || uc == ']') { 72 putc('%', fp); 73 putc(tab[(uc >> 4) & 0x0f], fp); 74 putc(tab[uc & 0x0f], fp); 75 } else { 76 putc(uc, fp); 77 } 78 } 79 } 80 81 /* Escape characters below as HTML 2.0 / XML 1.0. */ 82 void xmlencode(FILE *fp, const char *s, size_t len) { 83 size_t i; 84 for (i = 0; *s && i < len; s++, i++) { 85 switch (*s) { 86 case '<': 87 fputs("<", fp); 88 break; 89 case '>': 90 fputs(">", fp); 91 break; 92 case '\'': 93 fputs("'", fp); 94 break; 95 case '&': 96 fputs("&", fp); 97 break; 98 case '"': 99 fputs(""", fp); 100 break; 101 default: 102 putc(*s, fp); 103 } 104 } 105 } 106 107 void printtimeshort(FILE *fp, const git_time *intime) { 108 struct tm *intm; 109 time_t t; 110 char out[32]; 111 t = (time_t)intime->time; 112 if (!(intm = gmtime(&t))) 113 return; 114 strftime(out, sizeof(out), "%Y-%m-%d", intm); 115 fputs(out, fp); 116 } 117 118 void writeheader(FILE *fp) { 119 fputs("<!DOCTYPE html>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>", fp); 120 xmlencode(fp, description, strlen(description)); 121 fputs("</title>\n<meta name=\"description\" content=\"repositories\">\n" 122 "<meta name=\"keywords\" content=\"git, repositories\">\n" 123 "<meta name=\"author\" content=\"djnn\">\n", 124 fp); 125 fputs("<link rel=\"icon\" type=\"image/png\" href=\"/favicon.png\">\n" 126 "<link rel=\"stylesheet\" type=\"text/css\" href=\"/style.css\">\n", 127 fp); 128 fputs("<center><div class=\"container\">\n\t<center>\n\t<table>\n\t\t<tr><td>\n" 129 "<b>evil.djnn.sh ~ repositories</b>\n" 130 "\t\t</td></tr>\n\t</table>\n\t</center>\n</div></center>\n<br>\n", 131 fp); 132 fputs("<center><div id=\"content\">\n\t<center><table id=\"index\">\n\t\t<thead>\n\t\t\t<tr><td><b>name</b></td><td><b>description</b></td><td><b>last commit</b></td></tr>\n\t\t</thead>\n\t\t<tbody>", fp); 133 } 134 135 void writefooter(FILE *fp) { 136 fputs("\n\t\t</tbody>\n\t</table>\n</center>\n</div>\n<center>\n<br/>\n<div id=\"footer\">\n" 137 "\t© 2024 evil.djnn.sh • generated with stagit\n" 138 "</div>\n</center>", 139 fp); 140 } 141 142 int writelog(FILE *fp) { 143 git_commit *commit = NULL; 144 const git_signature *author; 145 git_revwalk *w = NULL; 146 git_oid id; 147 char *stripped_name = NULL, *p; 148 int ret = 0; 149 150 git_revwalk_new(&w, repo); 151 git_revwalk_push_head(w); 152 153 if (git_revwalk_next(&id, w) || 154 git_commit_lookup(&commit, repo, &id)) { 155 ret = -1; 156 goto err; 157 } 158 159 author = git_commit_author(commit); 160 161 /* strip .git suffix */ 162 if (!(stripped_name = strdup(name))) 163 err(1, "strdup"); 164 if ((p = strrchr(stripped_name, '.'))) 165 if (!strcmp(p, ".git")) 166 *p = '\0'; 167 168 fputs("\n\t\t\t<tr class=\"item-repo\"><td><a href=\"", fp); 169 percentencode(fp, stripped_name, strlen(stripped_name)); 170 fputs("/log.html\">", fp); 171 xmlencode(fp, stripped_name, strlen(stripped_name)); 172 fputs("</a></td><td>", fp); 173 xmlencode(fp, description, strlen(description)); 174 fputs("</td><td>", fp); 175 if (author) 176 printtimeshort(fp, &(author->when)); 177 fputs("</td></tr>", fp); 178 179 git_commit_free(commit); 180 err: 181 git_revwalk_free(w); 182 free(stripped_name); 183 184 return ret; 185 } 186 187 int main(int argc, char *argv[]) { 188 FILE *fp; 189 char path[PATH_MAX], repodirabs[PATH_MAX + 1]; 190 int i, ret = 0, nrepos = 0; 191 struct repoinfo *repos = NULL; 192 193 if (argc < 2) { 194 fprintf(stderr, "usage: %s [repodir...]\n", argv[0]); 195 return 1; 196 } 197 198 git_libgit2_init(); 199 for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) 200 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); 201 git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); 202 203 #ifdef __OpenBSD__ 204 if (pledge("stdio rpath", NULL) == -1) 205 err(1, "pledge"); 206 #endif 207 208 // Allocate space for repo info 209 repos = calloc(argc - 1, sizeof(struct repoinfo)); 210 if (!repos) 211 err(1, "calloc"); 212 213 // collect all repo info 214 for (i = 1; i < argc; i++) { 215 const char *repodir = argv[i]; 216 struct repoinfo *ri = &repos[nrepos]; 217 218 if (!realpath(repodir, repodirabs)) 219 err(1, "realpath"); 220 strncpy(ri->path, repodir, sizeof(ri->path)); 221 ri->path[sizeof(ri->path) - 1] = 0; 222 const char *slash = strrchr(repodirabs, '/'); 223 strncpy(ri->name, slash ? slash + 1 : repodirabs, sizeof(ri->name)); 224 ri->name[sizeof(ri->name) - 1] = 0; 225 226 // Description 227 ri->description[0] = '\0'; 228 joinpath(path, sizeof(path), repodir, "description"); 229 if ((fp = fopen(path, "r"))) { 230 if (!fgets(ri->description, sizeof(ri->description), fp)) 231 ri->description[0] = '\0'; 232 checkfileerror(fp, "description", 'r'); 233 fclose(fp); 234 } else { 235 joinpath(path, sizeof(path), repodir, ".git/description"); 236 if ((fp = fopen(path, "r"))) { 237 if (!fgets(ri->description, sizeof(ri->description), fp)) 238 ri->description[0] = '\0'; 239 checkfileerror(fp, "description", 'r'); 240 fclose(fp); 241 } 242 } 243 244 ri->last_commit_time = get_last_commit_time(repodir); 245 246 nrepos++; 247 } 248 249 // sort by last_commit_time DESCENDING 250 int cmp_repos(const void *a, const void *b) { 251 const struct repoinfo *ra = a, *rb = b; 252 if (ra->last_commit_time < rb->last_commit_time) 253 return 1; 254 if (ra->last_commit_time > rb->last_commit_time) 255 return -1; 256 return strcmp(ra->name, rb->name); // fallback: alphabetical 257 } 258 qsort(repos, nrepos, sizeof(struct repoinfo), cmp_repos); 259 260 // Write output 261 writeheader(stdout); 262 for (i = 0; i < nrepos; i++) { 263 // open the repo, set globals as needed 264 if (git_repository_open_ext(&repo, repos[i].path, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) { 265 fprintf(stderr, "%s: cannot open repository\n", repos[i].path); 266 ret = 1; 267 continue; 268 } 269 name = repos[i].name; 270 strncpy(description, repos[i].description, sizeof(description)); 271 description[sizeof(description) - 1] = 0; 272 writelog(stdout); 273 git_repository_free(repo); 274 } 275 writefooter(stdout); 276 277 git_libgit2_shutdown(); 278 free(repos); 279 280 checkfileerror(stdout, "<stdout>", 'w'); 281 return ret; 282 }