dotfiles

My PC Dotfiles & Website

site/git/stagit/stagit-index.c (8305B)


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