|
|
stagit-gopher-index.c - stagit-gopher - static git page generator for gopher |
|
|
 |
git clone git://git.codemadness.org/stagit-gopher (git://git.codemadness.org) |
|
|
 |
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(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 } |
|