stagit-gopher-index.c - stagit-gopher - A git gopher frontend. (mirror)
(HTM) 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 }