commit c0e0ccdb636d9962622841f48a3e84c52dbbc67a
parent 4116626b696c0a4ee918e9b221b5724bca3451c2
Author: Chris Bracken <chris@bracken.jp>
Date: Thu, 9 May 2024 21:35:43 +0900
Add gopher output support
gitout now writes output in gopher format when the -G option is supplied
to either gitout or gitout_index.
Diffstat:
28 files changed, 1394 insertions(+), 2 deletions(-)
diff --git a/format.c b/format.c
@@ -1,6 +1,7 @@
#include "format.h"
#include <err.h>
+#include <stdlib.h>
#include <string.h>
#include <time.h>
@@ -110,3 +111,159 @@ void print_xml_encoded_len(FILE* out,
}
}
}
+
+void print_gopher_text(FILE* out, const char* str, bool output_lf) {
+ print_gopher_text_len(out, str, -1, output_lf);
+}
+
+void print_gopher_text_len(FILE* out,
+ const char* str,
+ ssize_t str_len,
+ bool output_lf) {
+ size_t len = (str_len >= 0) ? (size_t)str_len : strlen(str);
+ bool start_of_line = true;
+ for (size_t i = 0; i < len && str[i] != '\0'; i++) {
+ char c = str[i];
+ if (output_lf && start_of_line && c == '[') {
+ fprintf(out, "[|");
+ } else if (c == '\t') {
+ fprintf(out, " ");
+ } else if (c == '\r' || (!output_lf && c == '\n')) {
+ // Ignore.
+ } else {
+ fprintf(out, "%c", c);
+ }
+ start_of_line = (c == '\n');
+ }
+}
+
+void print_gopher_link(FILE* out, const char* str) {
+ for (size_t i = 0; str[i] != '\0'; i++) {
+ char c = str[i];
+ if (c == '|') {
+ fprintf(out, "\\|");
+ } else if (c == '\t') {
+ fprintf(out, " ");
+ } else if (c == '\r' || c == '\n') {
+ // Ignore.
+ } else {
+ fprintf(out, "%c", c);
+ }
+ }
+}
+
+static size_t utf8_char_length(const char* str) {
+ unsigned char c = (unsigned char)*str;
+ if (c < 0x80) {
+ return 1;
+ }
+ if ((c & 0xe0) == 0xc0) {
+ return 2;
+ }
+ if ((c & 0xf0) == 0xe0) {
+ return 3;
+ }
+ if ((c & 0xf8) == 0xf0) {
+ return 4;
+ }
+ // Invalid UTF-8 sequence. Treat as single byte.
+ return 1;
+}
+
+static bool is_zwj(const char* str) {
+ return str[0] == '\xe2' && str[1] == '\x80' && str[2] == '\x8d';
+}
+
+static bool is_variation_selector(const char* str) {
+ return str[0] == '\xef' && str[1] == '\xb8' && str[2] == '\x8f';
+}
+
+static size_t get_char_width(wchar_t c) {
+ // ASCII characters in the range [0-31] and [127-255] are control characters
+ // or non-printable characters
+ if ((c >= 0 && c <= 31) || (c >= 127 && c <= 255)) {
+ return 1;
+ }
+
+ // Emoji range.
+ if (c >= 0x1F300 && c <= 0x1F6FF) {
+ return 2;
+ }
+
+ // Extended pictographic characters.
+ if (c >= 0x1F900 && c <= 0x1F9FF) {
+ return 2;
+ }
+
+ // Hiragana and Katakana ranges.
+ if ((c >= 0x3040 && c <= 0x309F) || (c >= 0x30A0 && c <= 0x30FF)) {
+ return 2;
+ }
+
+ // Kanji range.
+ if (c >= 0x4E00 && c <= 0x9FFF) {
+ return 2;
+ }
+ return 1;
+}
+
+// TODO(cbracken): There has to be a better way.
+void print_gopher_link_padded(FILE* out,
+ const char* str,
+ size_t width,
+ char pad_char) {
+ size_t str_len = strlen(str);
+ size_t char_count = 0;
+ size_t last_char_width = 0;
+ size_t byte_count = 0;
+ while (byte_count < str_len) {
+ size_t pos = byte_count;
+ size_t char_length = utf8_char_length(&str[byte_count]);
+ byte_count += char_length;
+ if (char_count == width - 1 && byte_count < str_len) {
+ fprintf(out, "\u2026");
+ last_char_width = 1;
+ char_count += last_char_width;
+ } else if (char_count < width) {
+ if (str[pos] == '|') {
+ fprintf(out, "\\|");
+ char_count++;
+ last_char_width = 1;
+ } else if (str[pos] == '\t') {
+ for (size_t i = 0; i < 8 && char_count < width; i++) {
+ fprintf(out, " ");
+ char_count++;
+ }
+ last_char_width = 1;
+ } else if (str[pos] == '\r' || str[pos] == '\n') {
+ // Ignore.
+ last_char_width = 0;
+ } else if (char_length == 3 && is_zwj(&str[pos])) {
+ fprintf(out, "\xe2\x80\x8d");
+ char_count -= last_char_width;
+ last_char_width = 0;
+ } else if (char_length == 3 && is_variation_selector(&str[pos])) {
+ fprintf(out, "\xef\xb8\x8f");
+ char_count -= last_char_width;
+ last_char_width = 0;
+ } else {
+ for (size_t i = pos; i < byte_count; i++) {
+ fprintf(out, "%c", str[i]);
+ }
+ wchar_t wide_char;
+ if (mbtowc(&wide_char, &str[pos], MB_CUR_MAX) >= 0) {
+ last_char_width = get_char_width(wide_char);
+ } else {
+ last_char_width = 1;
+ }
+ char_count += last_char_width;
+ }
+ }
+ }
+ if (pad_char != '\0') {
+ while (char_count < width) {
+ fprintf(out, "%c", pad_char);
+ char_count++;
+ }
+ }
+}
diff --git a/format.h b/format.h
@@ -32,4 +32,16 @@ void print_xml_encoded_len(FILE* out,
ssize_t str_len,
bool output_crlf);
+void print_gopher_text(FILE* out, const char* str, bool output_lf);
+void print_gopher_text_len(FILE* out,
+ const char* str,
+ ssize_t str_len,
+ bool output_lf);
+
+void print_gopher_link(FILE* out, const char* str);
+void print_gopher_link_padded(FILE* out,
+ const char* str,
+ size_t width,
+ char pad_char);
+
#endif // GITOUT_FORMAT_H_
diff --git a/gitout.c b/gitout.c
@@ -81,6 +81,8 @@ GitoutOptions* gitout_options_create(int argc, const char* argv[]) {
options.baseurl = argv[++i];
} else if (argv[i][1] == 'H') {
options.writer_type = kRepoWriterTypeHtml;
+ } else if (argv[i][1] == 'G') {
+ options.writer_type = kRepoWriterTypeGopher;
}
}
// Must specify at least one repo directory.
diff --git a/gitout_index.c b/gitout_index.c
@@ -33,6 +33,8 @@ GitoutIndexOptions* gitout_index_options_create(int argc, const char* argv[]) {
options.repo_dirs[options.repo_dir_count++] = argv[i];
} else if (argv[i][1] == 'H') {
options.writer_type = kIndexWriterTypeHtml;
+ } else if (argv[i][1] == 'G') {
+ options.writer_type = kIndexWriterTypeGopher;
}
}
GitoutIndexOptions* options_out = ecalloc(1, sizeof(GitoutIndexOptions));
diff --git a/writer/BUILD.gn b/writer/BUILD.gn
@@ -4,7 +4,10 @@ source_set("index_writer") {
"index_writer.h",
]
configs += [ "//:gitout_config" ]
- deps = [ "//writer/html:index_writer" ]
+ deps = [
+ "//writer/gopher:index_writer",
+ "//writer/html:index_writer",
+ ]
public_deps = [ "//git" ]
}
@@ -14,6 +17,9 @@ source_set("repo_writer") {
"repo_writer.h",
]
configs += [ "//:gitout_config" ]
- deps = [ "//writer/html:repo_writer" ]
+ deps = [
+ "//writer/gopher:repo_writer",
+ "//writer/html:repo_writer",
+ ]
public_deps = [ "//git" ]
}
diff --git a/writer/gopher/BUILD.gn b/writer/gopher/BUILD.gn
@@ -0,0 +1,41 @@
+source_set("index_writer") {
+ sources = [
+ "index_writer.c",
+ "index_writer.h",
+ "repo_index.c",
+ "repo_index.h",
+ ]
+ configs += [ "//:gitout_config" ]
+ deps = [
+ "//:format",
+ "//:utils",
+ ]
+ public_deps = [ "//git" ]
+}
+
+source_set("repo_writer") {
+ sources = [
+ "commit.c",
+ "commit.h",
+ "fileblob.c",
+ "fileblob.h",
+ "files.c",
+ "files.h",
+ "log.c",
+ "log.h",
+ "page.c",
+ "page.h",
+ "refs.c",
+ "refs.h",
+ "repo_writer.c",
+ "repo_writer.h",
+ ]
+ configs += [ "//:gitout_config" ]
+ deps = [
+ "//:format",
+ "//:utils",
+ "//writer/atom",
+ "//writer/cache",
+ ]
+ public_deps = [ "//git" ]
+}
diff --git a/writer/gopher/commit.c b/writer/gopher/commit.c
@@ -0,0 +1,210 @@
+#include "writer/gopher/commit.h"
+
+#include <err.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "format.h"
+#include "git/delta.h"
+#include "utils.h"
+#include "writer/gopher/page.h"
+
+struct GopherCommit {
+ FILE* out;
+ GopherPage* page;
+};
+
+static void gopher_commit_write_summary(GopherCommit* commit,
+ const CommitInfo* ci);
+static void gopher_commit_write_diffstat(GopherCommit* commit,
+ const CommitInfo* ci);
+static void gopher_commit_write_diffstat_row(GopherCommit* commit,
+ const DeltaInfo* delta);
+static void gopher_commit_write_diff_content(GopherCommit* commit,
+ const CommitInfo* ci);
+static void gopher_commit_write_diff_delta(GopherCommit* commit,
+ const DeltaInfo* delta);
+static void gopher_commit_write_diff_hunk(GopherCommit* commit,
+ const Hunk* hunk);
+
+GopherCommit* gopher_commit_create(const RepoInfo* repo,
+ const char* oid,
+ const char* title) {
+ GopherCommit* commit = ecalloc(1, sizeof(GopherCommit));
+ char filename[PATH_MAX];
+ if (snprintf(filename, sizeof(filename), "%s.gph", oid) < 0) {
+ err(1, "snprintf");
+ }
+ char path[PATH_MAX];
+ path_concat(path, sizeof(path), "commit", filename);
+ commit->out = efopen(path, "w");
+ commit->page = gopher_page_create(commit->out, repo, title, "");
+ return commit;
+}
+
+void gopher_commit_free(GopherCommit* commit) {
+ if (!commit) {
+ return;
+ }
+ fclose(commit->out);
+ commit->out = NULL;
+ gopher_page_free(commit->page);
+ commit->page = NULL;
+ free(commit);
+}
+
+void gopher_commit_begin(GopherCommit* commit) {
+ gopher_page_begin(commit->page);
+}
+
+void gopher_commit_add_commit(GopherCommit* commit, const CommitInfo* ci) {
+ static const uint64_t kDiffMaxFiles = 1000;
+ static const uint64_t kDiffMaxDeltas = 1000;
+ static const uint64_t kDiffMaxDeltaLines = 100000;
+
+ gopher_commit_write_summary(commit, ci);
+
+ if (ci->deltas_len == 0) {
+ return;
+ }
+ if (ci->filecount > kDiffMaxFiles || ci->deltas_len > kDiffMaxDeltas ||
+ ci->addcount > kDiffMaxDeltaLines || ci->delcount > kDiffMaxDeltaLines) {
+ fprintf(commit->out, "Diff is too large, output suppressed.</pre>\n");
+ return;
+ }
+
+ gopher_commit_write_diffstat(commit, ci);
+ gopher_commit_write_diff_content(commit, ci);
+}
+
+void gopher_commit_end(GopherCommit* commit) {
+ gopher_page_end(commit->page);
+}
+
+void gopher_commit_write_summary(GopherCommit* commit, const CommitInfo* ci) {
+ FILE* out = commit->out;
+ fprintf(out, "[1|commit %s|../commit/%s.gph|server|port]\n", ci->oid,
+ ci->oid);
+
+ if (ci->parentoid[0] != '\0') {
+ const char* oid = ci->parentoid;
+ fprintf(out, "[1|parent %s|../commit/%s.gph|server|port]\n", oid, oid);
+ }
+
+ fprintf(out, "[h|Author: ");
+ print_gopher_link(commit->out, ci->author_name);
+ fprintf(out, " <");
+ print_gopher_link(commit->out, ci->author_email);
+ fprintf(out, ">|URL:mailto:");
+ print_gopher_link(commit->out, ci->author_email);
+ fprintf(out, "|server|port]\n");
+ fprintf(out, "Date: ");
+ print_time(out, ci->author_time, ci->author_timezone_offset);
+ fprintf(out, "\n");
+
+ if (ci->msg) {
+ fprintf(out, "\n");
+ print_gopher_text(commit->out, ci->msg, true);
+ fprintf(out, "\n");
+ }
+}
+
+void gopher_commit_write_diffstat(GopherCommit* commit, const CommitInfo* ci) {
+ fprintf(commit->out, "Diffstat:\n");
+ for (size_t i = 0; i < ci->deltas_len; i++) {
+ gopher_commit_write_diffstat_row(commit, ci->deltas[i]);
+ }
+}
+
+void gopher_commit_write_diffstat_row(GopherCommit* commit,
+ const DeltaInfo* delta) {
+ static const size_t kGraphWidth = 30;
+
+ fprintf(commit->out, " %c ", delta->status);
+ char filename[PATH_MAX];
+ if (strcmp(delta->old_file_path, delta->new_file_path) == 0) {
+ snprintf(filename, sizeof(filename), "%s", delta->old_file_path);
+ } else {
+ snprintf(filename, sizeof(filename), "%s -> %s", delta->old_file_path,
+ delta->new_file_path);
+ }
+ print_gopher_link_padded(commit->out, filename, 35, ' ');
+
+ size_t changed = delta->addcount + delta->delcount;
+ fprintf(commit->out, " | ");
+ fprintf(commit->out, "%7zu ", changed);
+ char* added_graph = deltainfo_added_graph(delta, kGraphWidth);
+ fprintf(commit->out, "%s", added_graph);
+ free(added_graph);
+ char* deleted_graph = deltainfo_deleted_graph(delta, kGraphWidth);
+ fprintf(commit->out, "%s", deleted_graph);
+ free(deleted_graph);
+ fprintf(commit->out, "\n");
+}
+
+void gopher_commit_write_diff_content(GopherCommit* commit,
+ const CommitInfo* ci) {
+ FILE* out = commit->out;
+ fprintf(out, "\n%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
+ ci->filecount, ci->filecount == 1 ? "" : "s", //
+ ci->addcount, ci->addcount == 1 ? "" : "s", //
+ ci->delcount, ci->delcount == 1 ? "" : "s");
+ fprintf(commit->out, "---\n");
+
+ for (size_t i = 0; i < ci->deltas_len; i++) {
+ gopher_commit_write_diff_delta(commit, ci->deltas[i]);
+ }
+}
+
+void gopher_commit_write_diff_delta(GopherCommit* commit,
+ const DeltaInfo* delta) {
+ FILE* out = commit->out;
+ fprintf(out, "[1|diff --git a/");
+ print_gopher_link(out, delta->old_file_path);
+ fprintf(out, " b/");
+ print_gopher_link(out, delta->new_file_path);
+ fprintf(out, "|../file/");
+ print_gopher_link(out, delta->new_file_path);
+ fprintf(out, ".gph|server|port]\n");
+
+ if (delta->is_binary) {
+ fprintf(out, "Binary files differ.\n");
+ } else {
+ for (size_t i = 0; i < delta->hunks_len; i++) {
+ gopher_commit_write_diff_hunk(commit, delta->hunks[i]);
+ }
+ }
+}
+
+void gopher_commit_write_diff_hunk(GopherCommit* commit, const Hunk* hunk) {
+ FILE* out = commit->out;
+
+ // Output header. e.g. @@ -0,0 +1,3 @@
+ print_gopher_text(out, hunk->header, false);
+ fprintf(out, "\n");
+
+ // Iterate over lines in hunk.
+ for (size_t i = 0; i < hunk->lines_len; i++) {
+ const HunkLine* line = hunk->lines[i];
+ if (line->old_lineno == -1) {
+ // Added line. Prefix with +.
+ fprintf(out, "+");
+ print_gopher_text_len(out, line->content, line->content_len, false);
+ fprintf(out, "\n");
+ } else if (line->new_lineno == -1) {
+ // Removed line. Prefix with -.
+ fprintf(out, "-");
+ print_gopher_text_len(out, line->content, line->content_len, false);
+ fprintf(out, "\n");
+ } else {
+ // Unchanged line. Prefix with ' '. No link.
+ fprintf(out, " ");
+ print_gopher_text_len(out, line->content, line->content_len, false);
+ fprintf(out, "\n");
+ }
+ }
+}
diff --git a/writer/gopher/commit.h b/writer/gopher/commit.h
@@ -0,0 +1,17 @@
+#ifndef GITOUT_WRITER_GOPHER_COMMIT_H_
+#define GITOUT_WRITER_GOPHER_COMMIT_H_
+
+#include "git/commit.h"
+#include "git/repo.h"
+
+typedef struct GopherCommit GopherCommit;
+
+GopherCommit* gopher_commit_create(const RepoInfo* repo,
+ const char* oid,
+ const char* title);
+void gopher_commit_free(GopherCommit* commit);
+void gopher_commit_begin(GopherCommit* commit);
+void gopher_commit_add_commit(GopherCommit* commit, const CommitInfo* ci);
+void gopher_commit_end(GopherCommit* commit);
+
+#endif // GITOUT_WRITER_GOPHER_COMMIT_H_
diff --git a/writer/gopher/fileblob.c b/writer/gopher/fileblob.c
@@ -0,0 +1,110 @@
+#include "writer/gopher/fileblob.h"
+
+#include <err.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "format.h"
+#include "utils.h"
+#include "writer/gopher/page.h"
+
+struct GopherFileBlob {
+ const RepoInfo* repo;
+ FILE* out;
+ GopherPage* page;
+};
+
+GopherFileBlob* gopher_fileblob_create(const RepoInfo* repo, const char* path) {
+ GopherFileBlob* blob = ecalloc(1, sizeof(GopherFileBlob));
+ blob->repo = repo;
+
+ // Create directories.
+ char filename[PATH_MAX];
+ if (snprintf(filename, sizeof(filename), "%s.gph", path) < 0) {
+ err(1, "snprintf");
+ }
+ char out_path[PATH_MAX];
+ path_concat(out_path, sizeof(out_path), "file", filename);
+ char dir[PATH_MAX];
+ estrlcpy(dir, out_path, sizeof(dir));
+ const char* d = dirname(dir);
+ if (!d) {
+ err(1, "dirname");
+ }
+ mkdirp(d);
+ blob->out = efopen(out_path, "w");
+
+ // Compute the relative path.
+ char relpath[PATH_MAX];
+ estrlcpy(relpath, "../", sizeof(relpath));
+ for (const char* p = d; *p != '\0'; p++) {
+ if (*p == '/') {
+ estrlcat(relpath, "../", sizeof(relpath));
+ }
+ }
+ estrlcpy(filename, path, sizeof(filename));
+ const char* title = basename(filename);
+ if (!title) {
+ err(1, "basename");
+ }
+ blob->page = gopher_page_create(blob->out, repo, title, relpath);
+ return blob;
+}
+
+void gopher_fileblob_free(GopherFileBlob* blob) {
+ if (!blob) {
+ return;
+ }
+ fclose(blob->out);
+ blob->out = NULL;
+ gopher_page_free(blob->page);
+ blob->page = NULL;
+ free(blob);
+}
+
+void gopher_fileblob_begin(GopherFileBlob* blob) {
+ gopher_page_begin(blob->page);
+}
+
+void gopher_fileblob_add_file(GopherFileBlob* blob, const FileInfo* fi) {
+ FILE* out = blob->out;
+
+ char path[PATH_MAX];
+ estrlcpy(path, fi->repo_path, sizeof(path));
+ const char* filename = basename(path);
+ if (!filename) {
+ err(1, "basename");
+ }
+ print_gopher_text(out, filename, false);
+ fprintf(out, " (%zdB)\n", fi->size_bytes);
+ fprintf(out, "---\n");
+
+ if (fi->size_lines < 0) {
+ fprintf(out, "Binary file.\n");
+ return;
+ }
+
+ size_t i = 0;
+ const char* content = fi->content;
+ const char* end = content + strlen(content);
+ const char* cur_line = content;
+ while (cur_line) {
+ const char* next_line = strchr(cur_line, '\n');
+ if (next_line || cur_line < end) {
+ size_t len = (next_line ? next_line : end) - cur_line;
+ i++;
+ fprintf(out, "%6zu ", i);
+ print_gopher_text_len(out, cur_line, len, false);
+ fprintf(out, "\n");
+ }
+ cur_line = next_line ? next_line + 1 : NULL;
+ }
+}
+
+void gopher_fileblob_end(GopherFileBlob* blob) {
+ gopher_page_end(blob->page);
+}
diff --git a/writer/gopher/fileblob.h b/writer/gopher/fileblob.h
@@ -0,0 +1,15 @@
+#ifndef GITOUT_WRITER_GOPHER_FILEBLOB_H_
+#define GITOUT_WRITER_GOPHER_FILEBLOB_H_
+
+#include "git/file.h"
+#include "git/repo.h"
+
+typedef struct GopherFileBlob GopherFileBlob;
+
+GopherFileBlob* gopher_fileblob_create(const RepoInfo* repo, const char* path);
+void gopher_fileblob_free(GopherFileBlob* blob);
+void gopher_fileblob_begin(GopherFileBlob* blob);
+void gopher_fileblob_add_file(GopherFileBlob* blob, const FileInfo* fi);
+void gopher_fileblob_end(GopherFileBlob* blob);
+
+#endif // GITOUT_WRITER_GOPHER_FILEBLOB_H_
diff --git a/writer/gopher/files.c b/writer/gopher/files.c
@@ -0,0 +1,61 @@
+#include "writer/gopher/files.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "format.h"
+#include "utils.h"
+#include "writer/gopher/page.h"
+
+struct GopherFiles {
+ const RepoInfo* repo;
+ FILE* out;
+ GopherPage* page;
+};
+
+GopherFiles* gopher_files_create(const RepoInfo* repo) {
+ GopherFiles* files = ecalloc(1, sizeof(GopherFiles));
+ files->repo = repo;
+ files->out = efopen("files.gph", "w");
+ files->page = gopher_page_create(files->out, repo, "Files", "");
+ return files;
+}
+
+void gopher_files_free(GopherFiles* files) {
+ if (!files) {
+ return;
+ }
+ fclose(files->out);
+ files->out = NULL;
+ gopher_page_free(files->page);
+ files->page = NULL;
+ free(files);
+}
+
+void gopher_files_begin(GopherFiles* files) {
+ FILE* out = files->out;
+ gopher_page_begin(files->page);
+ fprintf(out, "%-10.10s ", "Mode");
+ fprintf(out, "%-50.50s ", "Name");
+ fprintf(out, "%8.8s\n", "Size");
+}
+
+void gopher_files_add_file(GopherFiles* files, const FileInfo* fi) {
+ FILE* out = files->out;
+ fprintf(out, "[1|%s", fi->mode);
+ fprintf(out, " ");
+ print_gopher_link_padded(out, fi->display_path, 50, ' ');
+ fprintf(out, " ");
+ if (fi->size_lines >= 0) {
+ fprintf(files->out, "%7zdL", fi->size_lines);
+ } else if (fi->size_bytes >= 0) {
+ fprintf(files->out, "%7zdB", fi->size_bytes);
+ }
+ fprintf(out, "|file/");
+ print_gopher_link(out, fi->repo_path);
+ fprintf(out, ".gph|server|port]\n");
+}
+
+void gopher_files_end(GopherFiles* files) {
+ gopher_page_end(files->page);
+}
diff --git a/writer/gopher/files.h b/writer/gopher/files.h
@@ -0,0 +1,15 @@
+#ifndef GITOUT_WRITER_GOPHER_FILES_H_
+#define GITOUT_WRITER_GOPHER_FILES_H_
+
+#include "git/file.h"
+#include "git/repo.h"
+
+typedef struct GopherFiles GopherFiles;
+
+GopherFiles* gopher_files_create(const RepoInfo* repo);
+void gopher_files_free(GopherFiles* files);
+void gopher_files_begin(GopherFiles* files);
+void gopher_files_add_file(GopherFiles* files, const FileInfo* fi);
+void gopher_files_end(GopherFiles* files);
+
+#endif // GITOUT_WRITER_GOPHER_FILES_H_
diff --git a/writer/gopher/index_writer.c b/writer/gopher/index_writer.c
@@ -0,0 +1,43 @@
+#include "writer/gopher/index_writer.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "git/repo.h"
+#include "utils.h"
+#include "writer/gopher/repo_index.h"
+
+struct GopherIndexWriter {
+ GopherRepoIndex* index_;
+};
+
+GopherIndexWriter* gopher_indexwriter_create() {
+ GopherIndexWriter* writer = ecalloc(1, sizeof(GopherIndexWriter));
+ FILE* out = stdout;
+ writer->index_ = gopher_repoindex_create(out);
+ return writer;
+}
+
+void gopher_indexwriter_free(GopherIndexWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ gopher_repoindex_free(writer->index_);
+ writer->index_ = NULL;
+ free(writer);
+}
+
+void gopher_indexwriter_begin(void* writer) {
+ GopherIndexWriter* gopher_writer = (GopherIndexWriter*)writer;
+ gopher_repoindex_begin(gopher_writer->index_);
+}
+
+void gopher_indexwriter_add_repo(void* writer, RepoInfo* ri) {
+ GopherIndexWriter* gopher_writer = (GopherIndexWriter*)writer;
+ gopher_repoindex_add_repo(gopher_writer->index_, ri);
+}
+
+void gopher_indexwriter_end(void* writer) {
+ GopherIndexWriter* gopher_writer = (GopherIndexWriter*)writer;
+ gopher_repoindex_end(gopher_writer->index_);
+}
diff --git a/writer/gopher/index_writer.h b/writer/gopher/index_writer.h
@@ -0,0 +1,14 @@
+#ifndef GITOUT_WRITER_GOPHER_INDEX_WRITER_H_
+#define GITOUT_WRITER_GOPHER_INDEX_WRITER_H_
+
+#include "git/repo.h"
+
+typedef struct GopherIndexWriter GopherIndexWriter;
+
+GopherIndexWriter* gopher_indexwriter_create();
+void gopher_indexwriter_free(GopherIndexWriter* writer);
+void gopher_indexwriter_begin(void* writer);
+void gopher_indexwriter_add_repo(void* writer, RepoInfo* ri);
+void gopher_indexwriter_end(void* writer);
+
+#endif // GITOUT_WRITER_GOPHER_INDEX_WRITER_H_
diff --git a/writer/gopher/log.c b/writer/gopher/log.c
@@ -0,0 +1,101 @@
+#include "writer/gopher/log.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "format.h"
+#include "utils.h"
+#include "writer/cache/cache.h"
+#include "writer/gopher/page.h"
+
+struct GopherLog {
+ const RepoInfo* repo;
+ FILE* out;
+ Cache* cache;
+ GopherPage* page;
+ size_t remaining_commits;
+ size_t unlogged_commits;
+};
+
+static void write_commit_row(FILE* out, const CommitInfo* ci);
+
+GopherLog* gopher_log_create(const RepoInfo* repo) {
+ GopherLog* log = ecalloc(1, sizeof(GopherLog));
+ log->repo = repo;
+ log->out = efopen("log.gph", "w");
+ log->page = gopher_page_create(log->out, repo, "Log", "");
+ log->remaining_commits = SIZE_MAX;
+ log->unlogged_commits = 0;
+ return log;
+}
+
+void gopher_log_free(GopherLog* log) {
+ if (!log) {
+ return;
+ }
+ fclose(log->out);
+ log->out = NULL;
+ cache_free(log->cache);
+ log->cache = NULL;
+ gopher_page_free(log->page);
+ log->page = NULL;
+ free(log);
+}
+
+void gopher_log_set_cachefile(GopherLog* log, const char* cachefile) {
+ log->cache = cache_create(cachefile, write_commit_row);
+}
+
+void gopher_log_set_commit_limit(GopherLog* log, size_t count) {
+ log->remaining_commits = count;
+}
+
+bool gopher_log_can_add_commits(const GopherLog* log) {
+ return !log->cache || cache_can_add_commits(log->cache);
+}
+
+void gopher_log_begin(GopherLog* log) {
+ FILE* out = log->out;
+ gopher_page_begin(log->page);
+ fprintf(out, "%-16.16s ", "Date");
+ fprintf(out, "%-40.40s ", "Commit message");
+ fprintf(out, "%s\n", "Author");
+}
+
+void gopher_log_add_commit(GopherLog* log, const CommitInfo* ci) {
+ if (log->cache) {
+ cache_add_commit_row(log->cache, ci);
+ } else if (log->remaining_commits > 0) {
+ write_commit_row(log->out, ci);
+ log->remaining_commits--;
+ } else {
+ log->unlogged_commits++;
+ }
+}
+
+void gopher_log_end(GopherLog* log) {
+ FILE* out = log->out;
+ if (log->cache) {
+ cache_write(log->cache);
+ cache_copy_log(log->cache, log->out);
+ } else if (log->unlogged_commits > 0) {
+ size_t count = log->unlogged_commits;
+ fprintf(out, "%16.16s %zu more commits remaining, fetch the repository\n",
+ "", count);
+ }
+ fprintf(out, "\n[0|Atom feed|atom.xml|server|port]\n");
+ fprintf(out, "[0|Atom feed (tags)|tags.xml|server|port]\n");
+ gopher_page_end(log->page);
+}
+
+void write_commit_row(FILE* out, const CommitInfo* ci) {
+ fprintf(out, "[1|");
+ print_time_short(out, ci->author_time);
+ fprintf(out, " ");
+ if (ci->summary) {
+ print_gopher_link_padded(out, ci->summary, 40, ' ');
+ fprintf(out, " ");
+ }
+ print_gopher_link(out, ci->author_name);
+ fprintf(out, "|commit/%s.gph|server|port]\n", ci->oid);
+}
diff --git a/writer/gopher/log.h b/writer/gopher/log.h
@@ -0,0 +1,21 @@
+#ifndef GITOUT_WRITER_GOPHER_LOG_H_
+#define GITOUT_WRITER_GOPHER_LOG_H_
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "git/commit.h"
+#include "git/repo.h"
+
+typedef struct GopherLog GopherLog;
+
+GopherLog* gopher_log_create(const RepoInfo* repo);
+void gopher_log_free(GopherLog* log);
+void gopher_log_set_cachefile(GopherLog* log, const char* cachefile);
+void gopher_log_set_commit_limit(GopherLog* log, size_t count);
+bool gopher_log_can_add_commits(const GopherLog* log);
+void gopher_log_begin(GopherLog* log);
+void gopher_log_add_commit(GopherLog* log, const CommitInfo* commit);
+void gopher_log_end(GopherLog* log);
+
+#endif // GITOUT_WRITER_GOPHER_LOG_H_
diff --git a/writer/gopher/page.c b/writer/gopher/page.c
@@ -0,0 +1,81 @@
+#include "writer/gopher/page.h"
+
+#include <stdbool.h>
+#include <stdlib.h>
+
+#include "format.h"
+#include "utils.h"
+
+struct GopherPage {
+ FILE* out;
+ const RepoInfo* repo;
+ char* title;
+ char* relpath;
+};
+
+GopherPage* gopher_page_create(FILE* out,
+ const RepoInfo* repo,
+ const char* title,
+ const char* relpath) {
+ GopherPage* page = ecalloc(1, sizeof(GopherPage));
+ page->out = out;
+ page->repo = repo;
+ page->title = estrdup(title);
+ page->relpath = estrdup(relpath);
+ return page;
+}
+
+void gopher_page_free(GopherPage* page) {
+ if (!page) {
+ return;
+ }
+ free(page->title);
+ page->title = NULL;
+ free(page->relpath);
+ page->relpath = NULL;
+ free(page);
+}
+
+void gopher_page_begin(GopherPage* page) {
+ FILE* out = page->out;
+ print_gopher_text(out, page->title, false);
+ if (page->title[0] != '\0' && page->repo->short_name[0] != '\0') {
+ fprintf(out, " - ");
+ print_gopher_text(out, page->repo->short_name, false);
+ }
+ if (page->title[0] != '\0' && page->repo->description[0] != '\0') {
+ fprintf(out, " - ");
+ print_gopher_text(out, page->repo->description, false);
+ fprintf(out, "\n");
+ }
+ if (page->repo->clone_url[0] != '\0') {
+ fprintf(out, "[h|git clone ");
+ print_gopher_link(out, page->repo->clone_url);
+ fprintf(out, "|URL:");
+ print_gopher_link(out, page->repo->clone_url);
+ fprintf(out, "|server|port]\n");
+ }
+ fprintf(out, "[1|Log|%slog.gph|server|port]\n", page->relpath);
+ fprintf(out, "[1|Files|%sfiles.gph|server|port]\n", page->relpath);
+ fprintf(out, "[1|Refs|%srefs.gph|server|port]\n", page->relpath);
+
+ if (page->repo->submodules[0] != '\0') {
+ fprintf(out, "[1|Submodules|%sfile/%s.gph|server|port]\n", page->relpath,
+ page->repo->submodules);
+ }
+ if (page->repo->readme[0] != '\0') {
+ fprintf(out, "[1|README|%sfile/%s.gph|server|port]\n", page->relpath,
+ page->repo->readme);
+ }
+ if (page->repo->license[0] != '\0') {
+ fprintf(out, "[1|LICENSE|%sfile/%s.gph|server|port]\n", page->relpath,
+ page->repo->license);
+ }
+ fprintf(out, "---\n");
+}
+
+void gopher_page_end(GopherPage* page) {
+ if (!page) {
+ return;
+ }
+}
diff --git a/writer/gopher/page.h b/writer/gopher/page.h
@@ -0,0 +1,18 @@
+#ifndef GITOUT_WRITER_GOPHER_PAGE_H_
+#define GITOUT_WRITER_GOPHER_PAGE_H_
+
+#include <stdio.h>
+
+#include "git/repo.h"
+
+typedef struct GopherPage GopherPage;
+
+GopherPage* gopher_page_create(FILE* out,
+ const RepoInfo* repo,
+ const char* title,
+ const char* relpath);
+void gopher_page_free(GopherPage* page);
+void gopher_page_begin(GopherPage* page);
+void gopher_page_end(GopherPage* page);
+
+#endif // GITOUT_WRITER_GOPHER_PAGE_H_
diff --git a/writer/gopher/refs.c b/writer/gopher/refs.c
@@ -0,0 +1,145 @@
+#include "writer/gopher/refs.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "format.h"
+#include "git/commit.h"
+#include "utils.h"
+#include "writer/gopher/page.h"
+
+typedef struct {
+ char* title;
+ char* id;
+ FILE* out;
+} GopherRefsTable;
+
+struct GopherRefs {
+ const RepoInfo* repo;
+ FILE* out;
+ GopherPage* page;
+ GopherRefsTable* branches;
+ GopherRefsTable* tags;
+};
+
+static GopherRefsTable* gopher_refstable_create(const char* title,
+ const char* id,
+ FILE* out);
+static void gopher_refstable_free(GopherRefsTable* table);
+static void gopher_refstable_begin(GopherRefsTable* table);
+static void gopher_refstable_add_ref(GopherRefsTable* table,
+ const ReferenceInfo* ri);
+static void gopher_refstable_end(GopherRefsTable* table);
+
+GopherRefsTable* gopher_refstable_create(const char* title,
+ const char* id,
+ FILE* out) {
+ GopherRefsTable* table = ecalloc(1, sizeof(GopherRefsTable));
+ table->title = estrdup(title);
+ table->id = estrdup(id);
+ table->out = out;
+ return table;
+}
+
+void gopher_refstable_free(GopherRefsTable* table) {
+ if (!table) {
+ return;
+ }
+ free(table->title);
+ table->title = NULL;
+ free(table->id);
+ table->id = NULL;
+ free(table);
+}
+
+void gopher_refstable_begin(GopherRefsTable* table) {
+ FILE* out = table->out;
+ fprintf(out, "%s\n", table->title);
+ fprintf(out, " %-32.32s", "Name");
+ fprintf(out, " %-16.16s", "Last commit date");
+ fprintf(out, " %s\n", "Author");
+}
+
+void gopher_refstable_add_ref(GopherRefsTable* table, const ReferenceInfo* ri) {
+ CommitInfo* ci = ri->ci;
+ FILE* out = table->out;
+
+ fprintf(out, " ");
+ print_gopher_link_padded(out, ri->shorthand, 32, ' ');
+ fprintf(out, " ");
+ print_time_short(out, ci->author_time);
+ fprintf(out, " ");
+ print_gopher_link_padded(out, ci->author_name, 25, '\0');
+ fprintf(out, "\n");
+}
+
+void gopher_refstable_end(GopherRefsTable* table) {
+ FILE* out = table->out;
+ fprintf(out, "\n");
+}
+
+GopherRefs* gopher_refs_create(const RepoInfo* repo) {
+ GopherRefs* refs = ecalloc(1, sizeof(GopherRefs));
+ refs->repo = repo;
+ refs->out = efopen("refs.gph", "w");
+ refs->page = gopher_page_create(refs->out, repo, "Refs", "");
+ return refs;
+}
+
+void gopher_refs_free(GopherRefs* refs) {
+ if (!refs) {
+ return;
+ }
+ fclose(refs->out);
+ refs->out = NULL;
+ gopher_page_free(refs->page);
+ refs->page = NULL;
+ gopher_refstable_free(refs->branches);
+ refs->branches = NULL;
+ gopher_refstable_free(refs->tags);
+ refs->tags = NULL;
+ free(refs);
+}
+
+void gopher_refs_begin(GopherRefs* refs) {
+ gopher_page_begin(refs->page);
+}
+
+void gopher_refs_add_ref(GopherRefs* refs, const ReferenceInfo* ri) {
+ switch (ri->type) {
+ case kReftypeBranch:
+ if (!refs->branches) {
+ refs->branches =
+ gopher_refstable_create("Branches", "branches", refs->out);
+ gopher_refstable_begin(refs->branches);
+ }
+ gopher_refstable_add_ref(refs->branches, ri);
+ break;
+ case kReftypeTag:
+ if (refs->branches) {
+ gopher_refstable_end(refs->branches);
+ gopher_refstable_free(refs->branches);
+ refs->branches = NULL;
+ }
+ if (!refs->tags) {
+ refs->tags = gopher_refstable_create("Tags", "tags", refs->out);
+ gopher_refstable_begin(refs->tags);
+ }
+ gopher_refstable_add_ref(refs->tags, ri);
+ break;
+ }
+}
+
+void gopher_refs_end(GopherRefs* refs) {
+ if (refs->branches) {
+ gopher_refstable_end(refs->branches);
+ gopher_refstable_free(refs->branches);
+ refs->branches = NULL;
+ }
+ if (refs->tags) {
+ gopher_refstable_end(refs->tags);
+ gopher_refstable_free(refs->tags);
+ refs->tags = NULL;
+ }
+ gopher_page_end(refs->page);
+}
diff --git a/writer/gopher/refs.h b/writer/gopher/refs.h
@@ -0,0 +1,15 @@
+#ifndef GITOUT_WRITER_GOPHER_REFS_H_
+#define GITOUT_WRITER_GOPHER_REFS_H_
+
+#include "git/reference.h"
+#include "git/repo.h"
+
+typedef struct GopherRefs GopherRefs;
+
+GopherRefs* gopher_refs_create(const RepoInfo* repo);
+void gopher_refs_free(GopherRefs* refs);
+void gopher_refs_begin(GopherRefs* refs);
+void gopher_refs_add_ref(GopherRefs* refs, const ReferenceInfo* ri);
+void gopher_refs_end(GopherRefs* refs);
+
+#endif // GITOUT_WRITER_GOPHER_REFS_H_
diff --git a/writer/gopher/repo_index.c b/writer/gopher/repo_index.c
@@ -0,0 +1,57 @@
+#include "writer/gopher/repo_index.h"
+
+#include <stdlib.h>
+
+#include "format.h"
+#include "git/commit.h"
+#include "utils.h"
+
+struct GopherRepoIndex {
+ FILE* out;
+};
+
+GopherRepoIndex* gopher_repoindex_create(FILE* out) {
+ GopherRepoIndex* index = ecalloc(1, sizeof(GopherRepoIndex));
+ index->out = out;
+ return index;
+}
+
+void gopher_repoindex_free(GopherRepoIndex* index) {
+ if (!index) {
+ return;
+ }
+ fclose(index->out);
+ index->out = NULL;
+ free(index);
+}
+
+void gopher_repoindex_begin(GopherRepoIndex* index) {
+ FILE* out = index->out;
+ fprintf(out, "Repositories\n\n");
+ fprintf(out, "%-20.20s ", "Name");
+ fprintf(out, "%-39.39s ", "Description");
+ fprintf(out, "%s\n", "Last commit");
+}
+
+static void print_author_time(const CommitInfo* ci, void* user_data) {
+ FILE* out = (FILE*)user_data;
+ print_time_short(out, ci->author_time);
+}
+
+void gopher_repoindex_add_repo(GopherRepoIndex* index, RepoInfo* ri) {
+ FILE* out = index->out;
+ fprintf(out, "[1|");
+ print_gopher_link_padded(out, ri->short_name, 20, ' ');
+ fprintf(out, " ");
+ print_gopher_link_padded(out, ri->description, 39, ' ');
+ fprintf(out, " ");
+ repoinfo_for_commit(ri, "HEAD", print_author_time, out);
+ fprintf(out, "|%s/log.gph|server|port]\n", ri->short_name);
+}
+
+void gopher_repoindex_end(GopherRepoIndex* index) {
+ // Stop the compiler from complaining about unused parameter.
+ if (!index) {
+ return;
+ }
+}
diff --git a/writer/gopher/repo_index.h b/writer/gopher/repo_index.h
@@ -0,0 +1,16 @@
+#ifndef GITOUT_WRITER_GOPHER_REPOINDEX_H_
+#define GITOUT_WRITER_GOPHER_REPOINDEX_H_
+
+#include "git/repo.h"
+
+#include <stdio.h>
+
+typedef struct GopherRepoIndex GopherRepoIndex;
+
+GopherRepoIndex* gopher_repoindex_create(FILE* out);
+void gopher_repoindex_free(GopherRepoIndex* index);
+void gopher_repoindex_begin(GopherRepoIndex* index);
+void gopher_repoindex_add_repo(GopherRepoIndex* index, RepoInfo* ri);
+void gopher_repoindex_end(GopherRepoIndex* index);
+
+#endif // GITOUT_WRITER_GOPHER_REPOINDEX_H_
diff --git a/writer/gopher/repo_writer.c b/writer/gopher/repo_writer.c
@@ -0,0 +1,140 @@
+#include "writer/gopher/repo_writer.h"
+
+#include <err.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "git/commit.h"
+#include "git/file.h"
+#include "git/reference.h"
+#include "utils.h"
+#include "writer/atom/atom.h"
+#include "writer/gopher/commit.h"
+#include "writer/gopher/fileblob.h"
+#include "writer/gopher/files.h"
+#include "writer/gopher/log.h"
+#include "writer/gopher/refs.h"
+
+struct GopherRepoWriter {
+ const RepoInfo* repo_;
+ GopherRefs* refs_;
+ GopherLog* log_;
+ Atom* atom_;
+ Atom* tags_;
+ GopherFiles* files_;
+};
+
+GopherRepoWriter* gopher_repowriter_create(const RepoInfo* repo) {
+ GopherRepoWriter* writer = ecalloc(1, sizeof(GopherRepoWriter));
+ writer->repo_ = repo;
+ writer->refs_ = gopher_refs_create(repo);
+ writer->log_ = gopher_log_create(repo);
+ writer->atom_ = atom_create(repo, kAtomTypeAll);
+ writer->tags_ = atom_create(repo, kAtomTypeTags);
+ writer->files_ = gopher_files_create(repo);
+ return writer;
+}
+
+void gopher_repowriter_free(GopherRepoWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ gopher_refs_free(writer->refs_);
+ writer->refs_ = NULL;
+ gopher_log_free(writer->log_);
+ writer->log_ = NULL;
+ atom_free(writer->atom_);
+ writer->atom_ = NULL;
+ atom_free(writer->tags_);
+ writer->tags_ = NULL;
+ gopher_files_free(writer->files_);
+ writer->files_ = NULL;
+ free(writer);
+}
+
+void gopher_repowriter_set_log_cachefile(void* writer, const char* cachefile) {
+ GopherRepoWriter* gopher_writer = (GopherRepoWriter*)writer;
+ gopher_log_set_cachefile(gopher_writer->log_, cachefile);
+}
+
+void gopher_repowriter_set_log_commit_limit(void* writer, size_t count) {
+ GopherRepoWriter* gopher_writer = (GopherRepoWriter*)writer;
+ gopher_log_set_commit_limit(gopher_writer->log_, count);
+}
+
+void gopher_repowriter_set_baseurl(void* writer, const char* baseurl) {
+ GopherRepoWriter* gopher_writer = (GopherRepoWriter*)writer;
+ atom_set_baseurl(gopher_writer->atom_, baseurl);
+ atom_set_baseurl(gopher_writer->tags_, baseurl);
+}
+
+void gopher_repowriter_begin(void* writer) {
+ GopherRepoWriter* gopher_writer = (GopherRepoWriter*)writer;
+ mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
+ mkdir("file", S_IRWXU | S_IRWXG | S_IRWXO);
+
+ gopher_refs_begin(gopher_writer->refs_);
+ gopher_log_begin(gopher_writer->log_);
+ atom_begin(gopher_writer->atom_);
+ atom_begin(gopher_writer->tags_);
+ gopher_files_begin(gopher_writer->files_);
+}
+
+void gopher_repowriter_add_commit(void* writer, const CommitInfo* ci) {
+ GopherRepoWriter* gopher_writer = (GopherRepoWriter*)writer;
+ char filename[PATH_MAX];
+ if (snprintf(filename, sizeof(filename), "%s.gph", ci->oid) < 0) {
+ err(1, "snprintf");
+ }
+ char path[PATH_MAX];
+ path_concat(path, sizeof(path), "commit", filename);
+ atom_add_commit(gopher_writer->atom_, ci, path, "", "");
+
+ if (gopher_log_can_add_commits(gopher_writer->log_)) {
+ gopher_log_add_commit(gopher_writer->log_, ci);
+ GopherCommit* commit =
+ gopher_commit_create(gopher_writer->repo_, ci->oid, ci->summary);
+ gopher_commit_begin(commit);
+ gopher_commit_add_commit(commit, ci);
+ gopher_commit_end(commit);
+ gopher_commit_free(commit);
+ }
+}
+
+void gopher_repowriter_add_reference(void* writer, const ReferenceInfo* ri) {
+ GopherRepoWriter* gopher_writer = (GopherRepoWriter*)writer;
+ gopher_refs_add_ref(gopher_writer->refs_, ri);
+ if (ri->type == kReftypeTag) {
+ char filename[PATH_MAX];
+ if (snprintf(filename, sizeof(filename), "%s.gph", ri->ci->oid) < 0) {
+ err(1, "snprintf");
+ }
+ char path[PATH_MAX];
+ path_concat(path, sizeof(path), "commit", filename);
+ atom_add_commit(gopher_writer->tags_, ri->ci, path, "", ri->shorthand);
+ }
+}
+
+void gopher_repowriter_add_file(void* writer, const FileInfo* fi) {
+ GopherRepoWriter* gopher_writer = (GopherRepoWriter*)writer;
+ gopher_files_add_file(gopher_writer->files_, fi);
+
+ GopherFileBlob* blob =
+ gopher_fileblob_create(gopher_writer->repo_, fi->repo_path);
+ gopher_fileblob_begin(blob);
+ gopher_fileblob_add_file(blob, fi);
+ gopher_fileblob_end(blob);
+ gopher_fileblob_free(blob);
+}
+
+void gopher_repowriter_end(void* writer) {
+ GopherRepoWriter* gopher_writer = (GopherRepoWriter*)writer;
+ gopher_refs_end(gopher_writer->refs_);
+ gopher_log_end(gopher_writer->log_);
+ atom_end(gopher_writer->atom_);
+ atom_end(gopher_writer->tags_);
+ gopher_files_end(gopher_writer->files_);
+}
diff --git a/writer/gopher/repo_writer.h b/writer/gopher/repo_writer.h
@@ -0,0 +1,24 @@
+#ifndef GITOUT_WRITER_GOPHER_REPO_WRITER_H_
+#define GITOUT_WRITER_GOPHER_REPO_WRITER_H_
+
+#include <stddef.h>
+
+#include "git/commit.h"
+#include "git/file.h"
+#include "git/reference.h"
+#include "git/repo.h"
+
+typedef struct GopherRepoWriter GopherRepoWriter;
+
+GopherRepoWriter* gopher_repowriter_create(const RepoInfo* repo);
+void gopher_repowriter_free(GopherRepoWriter* writer);
+void gopher_repowriter_set_log_cachefile(void* writer, const char* cachefile);
+void gopher_repowriter_set_log_commit_limit(void* writer, size_t count);
+void gopher_repowriter_set_baseurl(void* writer, const char* baseurl);
+void gopher_repowriter_begin(void* writer);
+void gopher_repowriter_add_commit(void* writer, const CommitInfo* ci);
+void gopher_repowriter_add_reference(void* writer, const ReferenceInfo* ri);
+void gopher_repowriter_add_file(void* writer, const FileInfo* fi);
+void gopher_repowriter_end(void* writer);
+
+#endif // GITOUT_WRITER_GOPHER_REPO_WRITER_H_
diff --git a/writer/index_writer.c b/writer/index_writer.c
@@ -4,6 +4,7 @@
#include <stdlib.h>
#include "utils.h"
+#include "writer/gopher/index_writer.h"
#include "writer/html/index_writer.h"
typedef void (*IndexWriterBegin)(void* impl);
@@ -24,10 +25,15 @@ struct IndexWriter {
static IndexWriter* htmlindexwriter_create();
static void htmlindexwriter_free(IndexWriter* writer);
+static IndexWriter* gopherindexwriter_create();
+static void gopherindexwriter_free(IndexWriter* writer);
+
IndexWriter* indexwriter_create(IndexWriterType type) {
switch (type) {
case kIndexWriterTypeHtml:
return htmlindexwriter_create();
+ case kIndexWriterTypeGopher:
+ return gopherindexwriter_create();
}
errx(1, "unknown IndexWriterType %d", type);
}
@@ -40,6 +46,9 @@ void indexwriter_free(IndexWriter* writer) {
case kIndexWriterTypeHtml:
htmlindexwriter_free(writer);
return;
+ case kIndexWriterTypeGopher:
+ gopherindexwriter_free(writer);
+ return;
}
errx(1, "unknown IndexWriterType %d", writer->type);
}
@@ -77,3 +86,25 @@ void htmlindexwriter_free(IndexWriter* writer) {
writer->impl = NULL;
free(writer);
}
+
+/* GopherIndexWriter setup/teardown. */
+
+IndexWriter* gopherindexwriter_create() {
+ IndexWriter* writer = ecalloc(1, sizeof(IndexWriter));
+ GopherIndexWriter* gopher_writer = gopher_indexwriter_create();
+ writer->type = kIndexWriterTypeGopher;
+ writer->impl = gopher_writer;
+ writer->begin = gopher_indexwriter_begin;
+ writer->add_repo = gopher_indexwriter_add_repo;
+ writer->end = gopher_indexwriter_end;
+ return writer;
+}
+
+void gopherindexwriter_free(IndexWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ gopher_indexwriter_free(writer->impl);
+ writer->impl = NULL;
+ free(writer);
+}
diff --git a/writer/index_writer.h b/writer/index_writer.h
@@ -5,6 +5,7 @@
typedef enum {
kIndexWriterTypeHtml,
+ kIndexWriterTypeGopher,
} IndexWriterType;
typedef struct IndexWriter IndexWriter;
diff --git a/writer/repo_writer.c b/writer/repo_writer.c
@@ -4,6 +4,7 @@
#include <stdlib.h>
#include "utils.h"
+#include "writer/gopher/repo_writer.h"
#include "writer/html/repo_writer.h"
typedef void (*RepoWriterSetLogCachefile)(void* writer, const char* cachefile);
@@ -36,10 +37,15 @@ struct RepoWriter {
static RepoWriter* htmlrepowriter_create(RepoInfo* ri);
static void htmlrepowriter_free(RepoWriter* writer);
+static RepoWriter* gopherrepowriter_create(RepoInfo* ri);
+static void gopherrepowriter_free(RepoWriter* writer);
+
RepoWriter* repowriter_create(RepoWriterType type, RepoInfo* ri) {
switch (type) {
case kRepoWriterTypeHtml:
return htmlrepowriter_create(ri);
+ case kRepoWriterTypeGopher:
+ return gopherrepowriter_create(ri);
}
errx(1, "unknown RepoWriterType %d", type);
}
@@ -52,6 +58,9 @@ void repowriter_free(RepoWriter* writer) {
case kRepoWriterTypeHtml:
htmlrepowriter_free(writer);
return;
+ case kRepoWriterTypeGopher:
+ gopherrepowriter_free(writer);
+ return;
}
errx(1, "unknown RepoWriterType %d", writer->type);
}
@@ -114,3 +123,30 @@ void htmlrepowriter_free(RepoWriter* writer) {
writer->impl = NULL;
free(writer);
}
+
+/* GopherRepoWriter setup/teardown. */
+
+RepoWriter* gopherrepowriter_create(RepoInfo* ri) {
+ RepoWriter* writer = ecalloc(1, sizeof(RepoWriter));
+ GopherRepoWriter* gopher_writer = gopher_repowriter_create(ri);
+ writer->type = kRepoWriterTypeGopher;
+ writer->impl = gopher_writer;
+ writer->set_log_cachefile = gopher_repowriter_set_log_cachefile;
+ writer->set_log_commit_limit = gopher_repowriter_set_log_commit_limit;
+ writer->set_baseurl = gopher_repowriter_set_baseurl;
+ writer->begin = gopher_repowriter_begin;
+ writer->add_commit = gopher_repowriter_add_commit;
+ writer->add_reference = gopher_repowriter_add_reference;
+ writer->add_file = gopher_repowriter_add_file;
+ writer->end = gopher_repowriter_end;
+ return writer;
+}
+
+void gopherrepowriter_free(RepoWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ gopher_repowriter_free(writer->impl);
+ writer->impl = NULL;
+ free(writer);
+}
diff --git a/writer/repo_writer.h b/writer/repo_writer.h
@@ -10,6 +10,7 @@
typedef enum {
kRepoWriterTypeHtml,
+ kRepoWriterTypeGopher,
} RepoWriterType;
typedef struct RepoWriter RepoWriter;