18 char oid[GIT_OID_HEXSZ + 1];
19 char parentoid[GIT_OID_HEXSZ + 1];
21 const git_signature *author;
25 git_diff_stats *stats;
29 git_tree *commit_tree;
30 git_tree *parent_tree;
37 static git_repository *repo;
39 static const char *relpath = "";
40 static const char *repodir;
42 static char name[255];
43 static char description[255];
44 static int hasreadme, haslicense;
47 commitinfo_free(struct commitinfo *ci)
52 git_diff_stats_free(ci->stats);
53 git_diff_free(ci->diff);
54 git_commit_free(ci->commit);
58 commitinfo_getbyoid(const git_oid *id)
60 struct commitinfo *ci;
63 if (!(ci = calloc(1, sizeof(struct commitinfo))))
67 if (git_commit_lookup(&(ci->commit), repo, id))
70 /* TODO: show tags when commit has it */
71 git_oid_tostr(ci->oid, sizeof(ci->oid), git_commit_id(ci->commit));
72 git_oid_tostr(ci->parentoid, sizeof(ci->parentoid), git_commit_parent_id(ci->commit, 0));
74 ci->author = git_commit_author(ci->commit);
75 ci->summary = git_commit_summary(ci->commit);
76 ci->msg = git_commit_message(ci->commit);
78 if ((error = git_commit_tree(&(ci->commit_tree), ci->commit)))
79 goto err; /* TODO: handle error */
80 if (!(error = git_commit_parent(&(ci->parent), ci->commit, 0))) {
81 if ((error = git_commit_tree(&(ci->parent_tree), ci->parent)))
85 ci->parent_tree = NULL;
88 if ((error = git_diff_tree_to_tree(&(ci->diff), repo, ci->parent_tree, ci->commit_tree, NULL)))
90 if (git_diff_get_stats(&(ci->stats), ci->diff))
93 ci->addcount = git_diff_stats_insertions(ci->stats);
94 ci->delcount = git_diff_stats_deletions(ci->stats);
95 ci->filecount = git_diff_stats_files_changed(ci->stats);
97 /* TODO: show tag when commit has it */
109 writeheader(FILE *fp)
111 fputs("<!DOCTYPE HTML>"
112 "<html dir=\"ltr\" lang=\"en\">\n<head>\n"
113 "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
114 "<meta http-equiv=\"Content-Language\" content=\"en\" />\n", fp);
115 fprintf(fp, "<title>%s%s%s</title>\n", name, description[0] ? " - " : "", description);
116 fprintf(fp, "<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n", relpath);
117 fprintf(fp, "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"%s Atom Feed\" href=\"%satom.xml\" />\n",
119 fprintf(fp, "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />\n", relpath);
120 fputs("</head>\n<body>\n\n", fp);
121 fprintf(fp, "<table><tr><td><img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" /></td>"
122 "<td><h1>%s</h1><span class=\"desc\">%s</span></td></tr><tr><td></td><td>\n",
123 relpath, name, description);
124 fprintf(fp, "<a href=\"%slog.html\">Log</a> | ", relpath);
125 fprintf(fp, "<a href=\"%sfiles.html\">Files</a>", relpath);
127 fprintf(fp, " | <a href=\"%sfile/README.html\">README</a>", relpath);
129 fprintf(fp, " | <a href=\"%sfile/LICENSE.html\">LICENSE</a>", relpath);
130 fputs("</td></tr></table>\n<hr/><div id=\"content\">\n", fp);
136 writefooter(FILE *fp)
138 return !fputs("</div></body>\n</html>", fp);
142 efopen(const char *name, const char *flags)
146 if (!(fp = fopen(name, flags)))
152 /* Escape characters below as HTML 2.0 / XML 1.0. */
154 xmlencode(FILE *fp, const char *s, size_t len)
158 for (i = 0; *s && i < len; s++, i++) {
160 case '<': fputs("<", fp); break;
161 case '>': fputs(">", fp); break;
162 case '\'': fputs("'", fp); break;
163 case '&': fputs("&", fp); break;
164 case '"': fputs(""", fp); break;
165 default: fputc(*s, fp);
170 /* Some implementations of basename(3) return a pointer to a static
171 * internal buffer (OpenBSD). Others modify the contents of `path` (POSIX).
172 * This is a wrapper function that is compatible with both versions.
173 * The program will error out if basename(3) failed, this can only happen
174 * with the OpenBSD version. */
176 xbasename(const char *path)
180 if (!(p = strdup(path)))
182 if (!(b = basename(p)))
184 if (!(b = strdup(b)))
192 mkdirp(const char *path)
194 char tmp[PATH_MAX], *p;
196 strlcpy(tmp, path, sizeof(tmp)); /* TODO: bring in libutil? */
197 for (p = tmp + (tmp[0] == '/'); *p; p++) {
201 if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
205 if (mkdir(tmp, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST)
211 printtimeformat(FILE *fp, const git_time *intime, const char *fmt)
217 t = (time_t) intime->time + (intime->offset * 60);
219 strftime(out, sizeof(out), fmt, intm);
224 printtimez(FILE *fp, const git_time *intime)
226 printtimeformat(fp, intime, "%Y-%m-%dT%H:%M:%SZ");
230 printtime(FILE *fp, const git_time *intime)
232 printtimeformat(fp, intime, "%a %b %e %T %Y");
236 printtimeshort(FILE *fp, const git_time *intime)
238 printtimeformat(fp, intime, "%Y-%m-%d %H:%M");
242 writeblobhtml(FILE *fp, const git_blob *blob)
246 char *nfmt = "<a href=\"#l%d\" id=\"l%d\">%d</a>\n";
247 const char *s = git_blob_rawcontent(blob);
248 git_off_t len = git_blob_rawsize(blob);
250 fputs("<table id=\"blob\"><tr><td class=\"num\"><pre>\n", fp);
253 fprintf(fp, nfmt, n, n, n);
254 while (i < len - 1) {
257 fprintf(fp, nfmt, n, n, n);
263 fputs("</pre></td><td><pre>\n", fp);
264 xmlencode(fp, s, (size_t)len);
265 fputs("</pre></td></tr></table>\n", fp);
269 printcommit(FILE *fp, struct commitinfo *ci)
271 /* TODO: show tag when commit has it */
272 fprintf(fp, "<b>commit</b> <a href=\"%scommit/%s.html\">%s</a>\n",
273 relpath, ci->oid, ci->oid);
275 if (ci->parentoid[0])
276 fprintf(fp, "<b>parent</b> <a href=\"%scommit/%s.html\">%s</a>\n",
277 relpath, ci->parentoid, ci->parentoid);
280 if ((count = (int)git_commit_parentcount(commit)) > 1) {
281 fprintf(fp, "<b>Merge:</b>");
282 for (i = 0; i < count; i++) {
283 git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
284 fprintf(fp, " <a href=\"%scommit/%s.html\">%s</a>",
291 fprintf(fp, "<b>Author:</b> ");
292 xmlencode(fp, ci->author->name, strlen(ci->author->name));
293 fprintf(fp, " <<a href=\"mailto:");
294 xmlencode(fp, ci->author->email, strlen(ci->author->email));
296 xmlencode(fp, ci->author->email, strlen(ci->author->email));
297 fputs("</a>>\n<b>Date:</b> ", fp);
298 printtime(fp, &(ci->author->when));
304 xmlencode(fp, ci->msg, strlen(ci->msg));
310 printshowfile(struct commitinfo *ci)
312 const git_diff_delta *delta;
313 const git_diff_hunk *hunk;
314 const git_diff_line *line;
317 size_t ndeltas, nhunks, nhunklines;
322 snprintf(path, sizeof(path), "commit/%s.html", ci->oid);
323 /* check if file exists if so skip it */
324 if (!access(path, F_OK))
327 fp = efopen(path, "w+b");
329 fputs("<pre>\n", fp);
332 memset(&statsbuf, 0, sizeof(statsbuf));
336 if (!git_diff_stats_to_buf(&statsbuf, ci->stats,
337 GIT_DIFF_STATS_FULL | GIT_DIFF_STATS_SHORT, 80)) {
338 if (statsbuf.ptr && statsbuf.ptr[0]) {
339 fprintf(fp, "<b>Diffstat:</b>\n");
340 fputs(statsbuf.ptr, fp);
347 ndeltas = git_diff_num_deltas(ci->diff);
348 for (i = 0; i < ndeltas; i++) {
349 if (git_patch_from_diff(&patch, ci->diff, i)) {
350 git_patch_free(patch);
351 break; /* TODO: handle error */
354 delta = git_patch_get_delta(patch);
355 fprintf(fp, "<b>diff --git a/<a href=\"%sfile/%s.html\">%s</a> b/<a href=\"%sfile/%s.html\">%s</a></b>\n",
356 relpath, delta->old_file.path, delta->old_file.path,
357 relpath, delta->new_file.path, delta->new_file.path);
359 /* check binary data */
360 if (delta->flags & GIT_DIFF_FLAG_BINARY) {
361 fputs("Binary files differ\n", fp);
362 git_patch_free(patch);
366 nhunks = git_patch_num_hunks(patch);
367 for (j = 0; j < nhunks; j++) {
368 if (git_patch_get_hunk(&hunk, &nhunklines, patch, j))
369 break; /* TODO: handle error ? */
371 fprintf(fp, "<span class=\"h\">%s</span>\n", hunk->header);
374 if (git_patch_get_line_in_hunk(&line, patch, j, k))
376 if (line->old_lineno == -1)
377 fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"i\">+",
379 else if (line->new_lineno == -1)
380 fprintf(fp, "<a href=\"#h%zu-%zu\" id=\"h%zu-%zu\" class=\"d\">-",
384 xmlencode(fp, line->content, line->content_len);
385 if (line->old_lineno == -1 || line->new_lineno == -1)
389 git_patch_free(patch);
391 git_buf_free(&statsbuf);
393 fputs( "</pre>\n", fp);
402 struct commitinfo *ci;
403 git_revwalk *w = NULL;
408 mkdir("commit", 0755);
410 git_revwalk_new(&w, repo);
411 git_revwalk_push_head(w);
412 git_revwalk_sorting(w, GIT_SORT_TIME);
413 git_revwalk_simplify_first_parent(w);
415 /* TODO: also make "expanded" log ? (with message body) */
416 fputs("<table id=\"log\"><thead>\n<tr><td>Age</td><td>Commit message</td><td>Author</td>"
417 "<td>Files</td><td>+</td><td>-</td></tr>\n</thead><tbody>\n", fp);
418 while (!git_revwalk_next(&id, w)) {
421 if (!(ci = commitinfo_getbyoid(&id)))
424 fputs("<tr><td>", fp);
426 printtimeshort(fp, &(ci->author->when));
427 fputs("</td><td>", fp);
429 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
430 if ((len = strlen(ci->summary)) > 79) {
431 xmlencode(fp, ci->summary, 76);
434 xmlencode(fp, ci->summary, len);
438 fputs("</td><td>", fp);
440 xmlencode(fp, ci->author->name, strlen(ci->author->name));
441 fputs("</td><td>", fp);
442 fprintf(fp, "%zu", ci->filecount);
443 fputs("</td><td>", fp);
444 fprintf(fp, "+%zu", ci->addcount);
445 fputs("</td><td>", fp);
446 fprintf(fp, "-%zu", ci->delcount);
447 fputs("</td></tr>\n", fp);
454 fprintf(fp, "</tbody></table>");
463 printcommitatom(FILE *fp, struct commitinfo *ci)
465 fputs("<entry>\n", fp);
467 fprintf(fp, "<id>%s</id>\n", ci->oid);
469 fputs("<updated>", fp);
470 printtimez(fp, &(ci->author->when));
471 fputs("</updated>\n", fp);
474 fputs("<title type=\"text\">", fp);
475 xmlencode(fp, ci->summary, strlen(ci->summary));
476 fputs("</title>\n", fp);
479 fputs("<content type=\"text\">", fp);
480 fprintf(fp, "commit %s\n", ci->oid);
481 if (ci->parentoid[0])
482 fprintf(fp, "parent %s\n", ci->parentoid);
485 if ((count = (int)git_commit_parentcount(commit)) > 1) {
486 fprintf(fp, "Merge:");
487 for (i = 0; i < count; i++) {
488 git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
489 fprintf(fp, " %s", buf);
496 fprintf(fp, "Author: ");
497 xmlencode(fp, ci->author->name, strlen(ci->author->name));
498 fprintf(fp, " <");
499 xmlencode(fp, ci->author->email, strlen(ci->author->email));
500 fprintf(fp, ">\nDate: ");
501 printtime(fp, &(ci->author->when));
506 xmlencode(fp, ci->msg, strlen(ci->msg));
507 fputs("\n</content>\n", fp);
509 fputs("<author><name>", fp);
510 xmlencode(fp, ci->author->name, strlen(ci->author->name));
511 fputs("</name>\n<email>", fp);
512 xmlencode(fp, ci->author->email, strlen(ci->author->email));
513 fputs("</email>\n</author>\n", fp);
515 fputs("</entry>\n", fp);
521 struct commitinfo *ci;
522 git_revwalk *w = NULL;
524 size_t i, m = 100; /* max */
526 fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", fp);
527 fputs("<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
528 xmlencode(fp, name, strlen(name));
529 fputs(", branch master</title>\n<subtitle>", fp);
531 xmlencode(fp, description, strlen(description));
532 fputs("</subtitle>\n", fp);
534 git_revwalk_new(&w, repo);
535 git_revwalk_push_head(w);
536 git_revwalk_sorting(w, GIT_SORT_TIME);
537 git_revwalk_simplify_first_parent(w);
539 for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
540 if (!(ci = commitinfo_getbyoid(&id)))
542 printcommitatom(fp, ci);
547 fputs("</feed>", fp);
553 writeblob(const git_index_entry *entry)
555 char fpath[PATH_MAX];
557 git_object *obj = NULL;
560 snprintf(fpath, sizeof(fpath), "file/%s.html", entry->path);
561 snprintf(ref, sizeof(ref), "HEAD:%s", entry->path);
563 if (git_revparse_single(&obj, repo, ref))
566 if (mkdirp(dirname(fpath)))
569 relpath = "../"; /* TODO: dynamic relpath based on number of /'s */
571 fp = efopen(fpath, "w+b");
573 fprintf(fp, "<p>%s (%" PRIu64 "b)</p><hr/>", entry->path, entry->file_size);
574 if (git_blob_is_binary((git_blob *)obj)) {
575 fprintf(fp, "<p>Binary file</p>\n");
577 writeblobhtml(fp, (git_blob *)obj);
581 git_object_free(obj);
593 const git_index_entry *entry;
597 fputs("<table id=\"files\"><thead>\n"
598 "<tr><td>Mode</td><td>Name</td><td>Size</td></tr>\n"
599 "</thead><tbody>\n", fp);
601 git_repository_index(&index, repo);
602 count = git_index_entrycount(index);
604 for (i = 0; i < count; i++) {
605 entry = git_index_get_byindex(index, i);
607 fputs("<tr><td>", fp);
608 fprintf(fp, "%u", entry->mode); /* TODO: fancy print, like: "-rw-r--r--" */
609 fprintf(fp, "</td><td><a href=\"%sfile/", relpath);
610 xmlencode(fp, entry->path, strlen(entry->path));
611 fputs(".html\">", fp);
612 xmlencode(fp, entry->path, strlen(entry->path));
613 fputs("</a></td><td>", fp);
614 fprintf(fp, "%" PRIu64, entry->file_size);
615 fputs("</td></tr>\n", fp);
620 fputs("</tbody></table>", fp);
626 main(int argc, char *argv[])
628 git_object *obj = NULL;
629 const git_error *e = NULL;
631 char path[PATH_MAX], *p;
635 fprintf(stderr, "%s <repodir>\n", argv[0]);
642 if ((status = git_repository_open_ext(&repo, repodir,
643 GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) < 0) {
645 fprintf(stderr, "error %d/%d: %s\n", status, e->klass, e->message);
649 /* use directory name as name */
650 p = xbasename(repodir);
651 snprintf(name, sizeof(name), "%s", p);
654 /* read description or .git/description */
655 snprintf(path, sizeof(path), "%s%s%s",
656 repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "description");
657 if (!(fpread = fopen(path, "r+b"))) {
658 snprintf(path, sizeof(path), "%s%s%s",
659 repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/description");
660 fpread = fopen(path, "r+b");
663 if (!fgets(description, sizeof(description), fpread))
664 description[0] = '\0';
669 haslicense = !git_revparse_single(&obj, repo, "HEAD:LICENSE");
670 git_object_free(obj);
672 hasreadme = !git_revparse_single(&obj, repo, "HEAD:README");
673 git_object_free(obj);
675 fp = efopen("log.html", "w+b");
681 fp = efopen("files.html", "w+b");
688 fp = efopen("atom.xml", "w+b");
693 git_repository_free(repo);
694 git_libgit2_shutdown();