repos/stagit

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