stagit-gopher-index.c - stagit-gopher - static git page generator for gopher | |
git clone git://git.codemadness.org/stagit-gopher | |
Log | |
Files | |
Refs | |
README | |
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(PA… | |
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… | |
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] != '/' ? "/" : "… | |
153 if (r < 0 || (size_t)r >= bufsiz) | |
154 errx(1, "path truncated: '%s%s%s'", | |
155 path, path[0] && path[strlen(path) - 1] != '/' ?… | |
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_n… | |
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", argv… | |
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 … | |
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", … | |
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/desc… | |
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… | |
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 } |