stagit-gopher-index.c - stagit-gopher - A git gopher frontend. (mirror) HTML git clone git://bitreich.org/stagit-gopher/ git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/stagit-gopher/ DIR Log DIR Files DIR Refs DIR Tags DIR README DIR LICENSE --- stagit-gopher-index.c (6805B) --- 1 #include <err.h> 2 #include <locale.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 #include <wchar.h> 10 11 #include <git2.h> 12 13 #define PAD_TRUNCATE_SYMBOL "\xe2\x80\xa6" /* symbol: "ellipsis" */ 14 #define UTF_INVALID_SYMBOL "\xef\xbf\xbd" /* symbol: "replacement" */ 15 16 static git_repository *repo; 17 18 static const char *relpath = ""; 19 20 static char description[255] = "Repositories"; 21 static char *name = ""; 22 23 /* Handle read or write errors for a FILE * stream */ 24 void 25 checkfileerror(FILE *fp, const char *name, int mode) 26 { 27 if (mode == 'r' && ferror(fp)) 28 errx(1, "read error: %s", name); 29 else if (mode == 'w' && (fflush(fp) || ferror(fp))) 30 errx(1, "write error: %s", name); 31 } 32 33 /* Format `len' columns of characters. If string is shorter pad the rest 34 * with characters `pad`. */ 35 int 36 utf8pad(char *buf, size_t bufsiz, const char *s, size_t len, int pad) 37 { 38 wchar_t wc; 39 size_t col = 0, i, slen, siz = 0; 40 int inc, rl, w; 41 42 if (!bufsiz) 43 return -1; 44 if (!len) { 45 buf[0] = '\0'; 46 return 0; 47 } 48 49 slen = strlen(s); 50 for (i = 0; i < slen; i += inc) { 51 inc = 1; /* next byte */ 52 if ((unsigned char)s[i] < 32) 53 continue; 54 55 rl = mbtowc(&wc, &s[i], slen - i < 4 ? slen - i : 4); 56 inc = rl; 57 if (rl < 0) { 58 mbtowc(NULL, NULL, 0); /* reset state */ 59 inc = 1; /* invalid, seek next byte */ 60 w = 1; /* replacement char is one width */ 61 } else if ((w = wcwidth(wc)) == -1) { 62 continue; 63 } 64 65 if (col + w > len || (col + w == len && s[i + inc])) { 66 if (siz + 4 >= bufsiz) 67 return -1; 68 memcpy(&buf[siz], PAD_TRUNCATE_SYMBOL, sizeof(PAD_TRUNCATE_SYMBOL) - 1); 69 siz += sizeof(PAD_TRUNCATE_SYMBOL) - 1; 70 buf[siz] = '\0'; 71 col++; 72 break; 73 } else if (rl < 0) { 74 if (siz + 4 >= bufsiz) 75 return -1; 76 memcpy(&buf[siz], UTF_INVALID_SYMBOL, sizeof(UTF_INVALID_SYMBOL) - 1); 77 siz += sizeof(UTF_INVALID_SYMBOL) - 1; 78 buf[siz] = '\0'; 79 col++; 80 continue; 81 } 82 if (siz + inc + 1 >= bufsiz) 83 return -1; 84 memcpy(&buf[siz], &s[i], inc); 85 siz += inc; 86 buf[siz] = '\0'; 87 col += w; 88 } 89 90 len -= col; 91 if (siz + len + 1 >= bufsiz) 92 return -1; 93 memset(&buf[siz], pad, len); 94 siz += len; 95 buf[siz] = '\0'; 96 97 return 0; 98 } 99 100 /* Escape characters in text in geomyidae .gph format, 101 newlines are ignored */ 102 void 103 gphtext(FILE *fp, const char *s, size_t len) 104 { 105 size_t i; 106 107 for (i = 0; *s && i < len; s++, i++) { 108 switch (*s) { 109 case '\r': /* ignore CR */ 110 case '\n': /* ignore LF */ 111 break; 112 case '\t': 113 fputs(" ", fp); 114 break; 115 default: 116 putc(*s, fp); 117 break; 118 } 119 } 120 } 121 122 /* Escape characters in links in geomyidae .gph format */ 123 void 124 gphlink(FILE *fp, const char *s, size_t len) 125 { 126 size_t i; 127 128 for (i = 0; *s && i < len; s++, i++) { 129 switch (*s) { 130 case '\r': /* ignore CR */ 131 case '\n': /* ignore LF */ 132 break; 133 case '\t': 134 fputs(" ", fp); 135 break; 136 case '|': /* escape separators */ 137 fputs("\\|", fp); 138 break; 139 default: 140 putc(*s, fp); 141 break; 142 } 143 } 144 } 145 146 void 147 joinpath(char *buf, size_t bufsiz, const char *path, const char *path2) 148 { 149 int r; 150 151 r = snprintf(buf, bufsiz, "%s%s%s", 152 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 153 if (r < 0 || (size_t)r >= bufsiz) 154 errx(1, "path truncated: '%s%s%s'", 155 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2); 156 } 157 158 void 159 printtimeshort(FILE *fp, const git_time *intime) 160 { 161 struct tm *intm; 162 time_t t; 163 char out[32]; 164 165 t = (time_t)intime->time; 166 if (!(intm = gmtime(&t))) 167 return; 168 strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm); 169 fputs(out, fp); 170 } 171 172 void 173 writeheader(FILE *fp) 174 { 175 if (description[0]) { 176 if (description[0] == '[') 177 fputs("[|", fp); 178 gphtext(fp, description, strlen(description)); 179 fputs("\n\n", fp); 180 } 181 182 fprintf(fp, "%-20.20s ", "Name"); 183 fprintf(fp, "%-39.39s ", "Description"); 184 fprintf(fp, "%s\n", "Last commit"); 185 } 186 187 int 188 writelog(FILE *fp) 189 { 190 git_commit *commit = NULL; 191 const git_signature *author; 192 git_revwalk *w = NULL; 193 git_oid id; 194 char *stripped_name = NULL, *p; 195 char buf[1024]; 196 int ret = 0; 197 198 git_revwalk_new(&w, repo); 199 git_revwalk_push_head(w); 200 201 if (git_revwalk_next(&id, w) || 202 git_commit_lookup(&commit, repo, &id)) { 203 ret = -1; 204 goto err; 205 } 206 207 author = git_commit_author(commit); 208 209 /* strip .git suffix */ 210 if (!(stripped_name = strdup(name))) 211 err(1, "strdup"); 212 if ((p = strrchr(stripped_name, '.'))) 213 if (!strcmp(p, ".git")) 214 *p = '\0'; 215 216 fputs("[1|", fp); 217 utf8pad(buf, sizeof(buf), stripped_name, 20, ' '); 218 gphlink(fp, buf, strlen(buf)); 219 fputs(" ", fp); 220 utf8pad(buf, sizeof(buf), description, 39, ' '); 221 gphlink(fp, buf, strlen(buf)); 222 fputs(" ", fp); 223 if (author) 224 printtimeshort(fp, &(author->when)); 225 fprintf(fp, "|%s/%s/log.gph|server|port]\n", relpath, stripped_name); 226 227 git_commit_free(commit); 228 err: 229 git_revwalk_free(w); 230 free(stripped_name); 231 232 return ret; 233 } 234 235 void 236 usage(const char *argv0) 237 { 238 fprintf(stderr, "usage: %s [-b baseprefix] [repodir...]\n", argv0); 239 exit(1); 240 } 241 242 int 243 main(int argc, char *argv[]) 244 { 245 FILE *fp; 246 char path[PATH_MAX], repodirabs[PATH_MAX + 1]; 247 const char *repodir = NULL; 248 int i, r, ret = 0; 249 250 setlocale(LC_CTYPE, ""); 251 252 /* do not search outside the git repository: 253 GIT_CONFIG_LEVEL_APP is the highest level currently */ 254 git_libgit2_init(); 255 for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) 256 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); 257 /* do not require the git repository to be owned by the current user */ 258 git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); 259 260 #ifdef __OpenBSD__ 261 if (pledge("stdio rpath", NULL) == -1) 262 err(1, "pledge"); 263 #endif 264 265 for (i = 1, r = 0; i < argc; i++) { 266 if (argv[i][0] == '-') { 267 if (argv[i][1] != 'b' || i + 1 >= argc) 268 usage(argv[0]); 269 relpath = argv[++i]; 270 continue; 271 } 272 273 if (r++ == 0) 274 writeheader(stdout); 275 276 repodir = argv[i]; 277 if (!realpath(repodir, repodirabs)) 278 err(1, "realpath"); 279 280 if (git_repository_open_ext(&repo, repodir, 281 GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) { 282 fprintf(stderr, "%s: cannot open repository\n", argv[0]); 283 ret = 1; 284 continue; 285 } 286 287 /* use directory name as name */ 288 if ((name = strrchr(repodirabs, '/'))) 289 name++; 290 else 291 name = ""; 292 293 /* read description or .git/description */ 294 joinpath(path, sizeof(path), repodir, "description"); 295 if (!(fp = fopen(path, "r"))) { 296 joinpath(path, sizeof(path), repodir, ".git/description"); 297 fp = fopen(path, "r"); 298 } 299 description[0] = '\0'; 300 if (fp) { 301 if (fgets(description, sizeof(description), fp)) 302 description[strcspn(description, "\t\r\n")] = '\0'; 303 else 304 description[0] = '\0'; 305 checkfileerror(fp, "description", 'r'); 306 fclose(fp); 307 } 308 309 writelog(stdout); 310 } 311 if (!repodir) 312 usage(argv[0]); 313 314 /* cleanup */ 315 git_repository_free(repo); 316 git_libgit2_shutdown(); 317 318 checkfileerror(stdout, "<stdout>", 'w'); 319 320 return ret; 321 }