stagit


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("&lt;", fp);
     88             break;
     89         case '>':
     90             fputs("&gt;", fp);
     91             break;
     92         case '\'':
     93             fputs("&#39;", fp);
     94             break;
     95         case '&':
     96             fputs("&amp;", fp);
     97             break;
     98         case '"':
     99             fputs("&quot;", 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&copy; 2024 evil.djnn.sh &bull; 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 }