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