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>"
417 "<td>Author</td><td>Files</td><td class=\"num\">+</td>"
418 "<td class=\"num\">-</td></tr>\n</thead><tbody>\n", fp);
419 while (!git_revwalk_next(&id, w)) {
422 if (!(ci = commitinfo_getbyoid(&id)))
425 fputs("<tr><td>", fp);
427 printtimeshort(fp, &(ci->author->when));
428 fputs("</td><td>", fp);
430 fprintf(fp, "<a href=\"%scommit/%s.html\">", relpath, ci->oid);
431 if ((len = strlen(ci->summary)) > 79) {
432 xmlencode(fp, ci->summary, 76);
435 xmlencode(fp, ci->summary, len);
439 fputs("</td><td>", fp);
441 xmlencode(fp, ci->author->name, strlen(ci->author->name));
442 fputs("</td><td class=\"num\">", fp);
443 fprintf(fp, "%zu", ci->filecount);
444 fputs("</td><td class=\"num\">", fp);
445 fprintf(fp, "+%zu", ci->addcount);
446 fputs("</td><td class=\"num\">", fp);
447 fprintf(fp, "-%zu", ci->delcount);
448 fputs("</td></tr>\n", fp);
455 fprintf(fp, "</tbody></table>");
464 printcommitatom(FILE *fp, struct commitinfo *ci)
466 fputs("<entry>\n", fp);
468 fprintf(fp, "<id>%s</id>\n", ci->oid);
470 fputs("<updated>", fp);
471 printtimez(fp, &(ci->author->when));
472 fputs("</updated>\n", fp);
475 fputs("<title type=\"text\">", fp);
476 xmlencode(fp, ci->summary, strlen(ci->summary));
477 fputs("</title>\n", fp);
480 fputs("<content type=\"text\">", fp);
481 fprintf(fp, "commit %s\n", ci->oid);
482 if (ci->parentoid[0])
483 fprintf(fp, "parent %s\n", ci->parentoid);
486 if ((count = (int)git_commit_parentcount(commit)) > 1) {
487 fprintf(fp, "Merge:");
488 for (i = 0; i < count; i++) {
489 git_oid_tostr(buf, 8, git_commit_parent_id(commit, i));
490 fprintf(fp, " %s", buf);
497 fprintf(fp, "Author: ");
498 xmlencode(fp, ci->author->name, strlen(ci->author->name));
499 fprintf(fp, " <");
500 xmlencode(fp, ci->author->email, strlen(ci->author->email));
501 fprintf(fp, ">\nDate: ");
502 printtime(fp, &(ci->author->when));
507 xmlencode(fp, ci->msg, strlen(ci->msg));
508 fputs("\n</content>\n", fp);
510 fputs("<author><name>", fp);
511 xmlencode(fp, ci->author->name, strlen(ci->author->name));
512 fputs("</name>\n<email>", fp);
513 xmlencode(fp, ci->author->email, strlen(ci->author->email));
514 fputs("</email>\n</author>\n", fp);
516 fputs("</entry>\n", fp);
522 struct commitinfo *ci;
523 git_revwalk *w = NULL;
525 size_t i, m = 100; /* max */
527 fputs("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", fp);
528 fputs("<feed xmlns=\"http://www.w3.org/2005/Atom\">\n<title>", fp);
529 xmlencode(fp, name, strlen(name));
530 fputs(", branch master</title>\n<subtitle>", fp);
532 xmlencode(fp, description, strlen(description));
533 fputs("</subtitle>\n", fp);
535 git_revwalk_new(&w, repo);
536 git_revwalk_push_head(w);
537 git_revwalk_sorting(w, GIT_SORT_TIME);
538 git_revwalk_simplify_first_parent(w);
540 for (i = 0; i < m && !git_revwalk_next(&id, w); i++) {
541 if (!(ci = commitinfo_getbyoid(&id)))
543 printcommitatom(fp, ci);
548 fputs("</feed>", fp);
554 writeblob(const git_index_entry *entry)
556 char fpath[PATH_MAX];
558 char tmp[PATH_MAX] = "";
560 git_object *obj = NULL;
563 snprintf(fpath, sizeof(fpath), "file/%s.html", entry->path);
564 snprintf(ref, sizeof(ref), "HEAD:%s", entry->path);
566 if (git_revparse_single(&obj, repo, ref))
569 if (mkdirp(dirname(fpath)))
575 strlcat(tmp, "../", sizeof(tmp));
580 fp = efopen(fpath, "w+b");
582 fprintf(fp, "<p>%s (%" PRIu64 "b)</p><hr/>", entry->path, entry->file_size);
583 if (git_blob_is_binary((git_blob *)obj)) {
584 fprintf(fp, "<p>Binary file</p>\n");
586 writeblobhtml(fp, (git_blob *)obj);
590 git_object_free(obj);
602 const git_index_entry *entry;
606 fputs("<table id=\"files\"><thead>\n"
607 "<tr><td>Mode</td><td>Name</td><td>Size</td></tr>\n"
608 "</thead><tbody>\n", fp);
610 git_repository_index(&index, repo);
611 count = git_index_entrycount(index);
613 for (i = 0; i < count; i++) {
614 entry = git_index_get_byindex(index, i);
616 fputs("<tr><td>", fp);
617 fprintf(fp, "%u", entry->mode); /* TODO: fancy print, like: "-rw-r--r--" */
618 fprintf(fp, "</td><td><a href=\"%sfile/", relpath);
619 xmlencode(fp, entry->path, strlen(entry->path));
620 fputs(".html\">", fp);
621 xmlencode(fp, entry->path, strlen(entry->path));
622 fputs("</a></td><td class=\"num\">", fp);
623 fprintf(fp, "%" PRIu64, entry->file_size);
624 fputs("</td></tr>\n", fp);
629 fputs("</tbody></table>", fp);
635 main(int argc, char *argv[])
637 git_object *obj = NULL;
638 const git_error *e = NULL;
640 char path[PATH_MAX], *p;
644 fprintf(stderr, "%s <repodir>\n", argv[0]);
651 if ((status = git_repository_open_ext(&repo, repodir,
652 GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) < 0) {
654 fprintf(stderr, "error %d/%d: %s\n", status, e->klass, e->message);
658 /* use directory name as name */
659 p = xbasename(repodir);
660 snprintf(name, sizeof(name), "%s", p);
663 /* read description or .git/description */
664 snprintf(path, sizeof(path), "%s%s%s",
665 repodir, repodir[strlen(repodir)] == '/' ? "" : "/", "description");
666 if (!(fpread = fopen(path, "r+b"))) {
667 snprintf(path, sizeof(path), "%s%s%s",
668 repodir, repodir[strlen(repodir)] == '/' ? "" : "/", ".git/description");
669 fpread = fopen(path, "r+b");
672 if (!fgets(description, sizeof(description), fpread))
673 description[0] = '\0';
678 haslicense = !git_revparse_single(&obj, repo, "HEAD:LICENSE");
679 git_object_free(obj);
681 hasreadme = !git_revparse_single(&obj, repo, "HEAD:README");
682 git_object_free(obj);
684 fp = efopen("log.html", "w+b");
690 fp = efopen("files.html", "w+b");
697 fp = efopen("atom.xml", "w+b");
702 git_repository_free(repo);
703 git_libgit2_shutdown();