stagit-gemini-index.c - stagit-gemini - Stagit for gemini protocol Openbsd
(HTM) git clone git://thinkerwim.org/stagit-gemini.git
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
stagit-gemini-index.c (6556B)
---
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 .gmi format,
101 newlines are ignored */
102 void
103 gmitext(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 default:
113 putc(*s, fp);
114 break;
115 }
116 }
117 }
118
119 /* Escape characters in links in geomyidae .gmi format */
120 void
121 gmilink(FILE *fp, const char *s, size_t len)
122 {
123 size_t i;
124
125 for (i = 0; *s && i < len; s++, i++) {
126 switch (*s) {
127 case '\r': /* ignore CR */
128 case '\n': /* ignore LF */
129 break;
130 default:
131 putc(*s, fp);
132 break;
133 }
134 }
135 }
136
137 void
138 joinpath(char *buf, size_t bufsiz, const char *path, const char *path2)
139 {
140 int r;
141
142 r = snprintf(buf, bufsiz, "%s%s%s",
143 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
144 if (r < 0 || (size_t)r >= bufsiz)
145 errx(1, "path truncated: '%s%s%s'",
146 path, path[0] && path[strlen(path) - 1] != '/' ? "/" : "", path2);
147 }
148
149 void
150 printtimeshort(FILE *fp, const git_time *intime)
151 {
152 struct tm *intm;
153 time_t t;
154 char out[32];
155
156 t = (time_t)intime->time;
157 if (!(intm = gmtime(&t)))
158 return;
159 strftime(out, sizeof(out), "%Y-%m-%d %H:%M", intm);
160 fputs(out, fp);
161 }
162
163 void
164 writeheader(FILE *fp)
165 {
166 if (description[0]) {
167 fputs(description, fp);
168 fputs("\n\n", fp);
169 }
170
171 fprintf(fp, "%-20.20s ", "Name");
172 fprintf(fp, "%-39.39s ", "Description");
173 fprintf(fp, "%s\n", "Last commit");
174 }
175
176 int
177 writelog(FILE *fp)
178 {
179 git_commit *commit = NULL;
180 const git_signature *author;
181 git_revwalk *w = NULL;
182 git_oid id;
183 char *stripped_name = NULL, *p;
184 char buf[1024];
185 int ret = 0;
186
187 git_revwalk_new(&w, repo);
188 git_revwalk_push_head(w);
189
190 if (git_revwalk_next(&id, w) ||
191 git_commit_lookup(&commit, repo, &id)) {
192 ret = -1;
193 goto err;
194 }
195
196 author = git_commit_author(commit);
197
198 /* strip .git suffix */
199 if (!(stripped_name = strdup(name)))
200 err(1, "strdup");
201 if ((p = strrchr(stripped_name, '.')))
202 if (!strcmp(p, ".git"))
203 *p = '\0';
204
205 fprintf(fp, "=> %s/%s/log.gmi ", relpath, stripped_name);
206 utf8pad(buf, sizeof(buf), stripped_name, 20, ' ');
207 gmilink(fp, buf, strlen(buf));
208 fputs(" ", fp);
209 utf8pad(buf, sizeof(buf), description, 39, ' ');
210 gmilink(fp, buf, strlen(buf));
211 fputs(" ", fp);
212 if (author)
213 printtimeshort(fp, &(author->when));
214 fputc('\n', fp);
215
216 git_commit_free(commit);
217 err:
218 git_revwalk_free(w);
219 free(stripped_name);
220
221 return ret;
222 }
223
224 void
225 usage(const char *argv0)
226 {
227 fprintf(stderr, "usage: %s [-b baseprefix] [repodir...]\n", argv0);
228 exit(1);
229 }
230
231 int
232 main(int argc, char *argv[])
233 {
234 FILE *fp;
235 char path[PATH_MAX], repodirabs[PATH_MAX + 1];
236 const char *repodir = NULL;
237 int i, r, ret = 0;
238
239 setlocale(LC_CTYPE, "");
240
241 /* do not search outside the git repository:
242 GIT_CONFIG_LEVEL_APP is the highest level currently */
243 git_libgit2_init();
244 for (i = 1; i <= GIT_CONFIG_LEVEL_APP; i++)
245 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
246 /* do not require the git repository to be owned by the current user */
247 git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
248
249 #ifdef __OpenBSD__
250 if (pledge("stdio rpath", NULL) == -1)
251 err(1, "pledge");
252 #endif
253
254 for (i = 1, r = 0; i < argc; i++) {
255 if (argv[i][0] == '-') {
256 if (argv[i][1] != 'b' || i + 1 >= argc)
257 usage(argv[0]);
258 relpath = argv[++i];
259 continue;
260 }
261
262 if (r++ == 0)
263 writeheader(stdout);
264
265 repodir = argv[i];
266 if (!realpath(repodir, repodirabs))
267 err(1, "realpath");
268
269 if (git_repository_open_ext(&repo, repodir,
270 GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) {
271 fprintf(stderr, "%s: cannot open repository\n", argv[0]);
272 ret = 1;
273 continue;
274 }
275
276 /* use directory name as name */
277 if ((name = strrchr(repodirabs, '/')))
278 name++;
279 else
280 name = "";
281
282 /* read description or .git/description */
283 joinpath(path, sizeof(path), repodir, "description");
284 if (!(fp = fopen(path, "r"))) {
285 joinpath(path, sizeof(path), repodir, ".git/description");
286 fp = fopen(path, "r");
287 }
288 description[0] = '\0';
289 if (fp) {
290 if (fgets(description, sizeof(description), fp))
291 description[strcspn(description, "\t\r\n")] = '\0';
292 else
293 description[0] = '\0';
294 checkfileerror(fp, "description", 'r');
295 fclose(fp);
296 }
297
298 writelog(stdout);
299 }
300 if (!repodir)
301 usage(argv[0]);
302
303 /* cleanup */
304 git_repository_free(repo);
305 git_libgit2_shutdown();
306
307 checkfileerror(stdout, "<stdout>", 'w');
308
309 return ret;
310 }