commit e0932dba5cbf0d1e125989bb0fc9ad71ba5168fd
parent 5cdcd66c829a38c1a527ea8b1d83b0b32806915c
Author: Chris Bracken <chris@bracken.jp>
Date: Tue, 17 Feb 2026 19:08:58 +0900
git: extract Git and make structs public
This extracts a Git type that's a top-level wrapper around the libgit2
functionality we care about. For all other Git types, we make them
public plain old datatype C structs and eliminate all the access
functions, allowing us to work with the structs directly.
Structs now own all their fields, so this introduces string copies in
case the structs outlive the underlying libgit2 objects from which they
were produced.
This also extracts a FileSystem type which allows us to inject a
memory-based filesystem for better testing. Similarly, the Git type
allows us to make the HTML and gopher writers unit-testable without
having to rely on a real git repo.
Finally, this adds a ton of tests for the entire system.
Reformat
Diffstat:
90 files changed, 3497 insertions(+), 812 deletions(-)
diff --git a/BUILD.gn b/BUILD.gn
@@ -68,11 +68,34 @@ executable("gout_tests") {
sources = [
"src/format_tests.c",
+ "src/fs_inmemory.c",
"src/git/delta_tests.c",
"src/git/commit_tests.c",
+ "src/gout_index_options_tests.c",
+ "src/gout_options_tests.c",
+ "src/git/repo_tests.c",
"src/gout_tests.c",
"src/gout_tests_main.c",
"src/utils_tests.c",
+ "src/writer/atom/atom_tests.c",
+ "src/writer/cache/cache_tests.c",
+ "src/writer/gopher/log_tests.c",
+ "src/writer/gopher/refs_tests.c",
+ "src/writer/gopher/files_tests.c",
+ "src/writer/gopher/fileblob_tests.c",
+ "src/writer/gopher/commit_tests.c",
+ "src/writer/gopher/repo_index_tests.c",
+ "src/writer/gopher/repo_writer_tests.c",
+ "src/writer/gopher/page_tests.c",
+ "src/writer/html/log_tests.c",
+ "src/writer/html/files_tests.c",
+ "src/writer/html/fileblob_tests.c",
+ "src/writer/html/refs_tests.c",
+ "src/writer/html/commit_tests.c",
+ "src/writer/html/repo_index_tests.c",
+ "src/writer/html/repo_writer_tests.c",
+ "src/writer/html/page_tests.c",
+ "src/writer/index_writer_tests.c",
]
configs += [
":gout_config",
@@ -80,6 +103,7 @@ executable("gout_tests") {
"//build:test_warnings",
]
deps = [
+ "//src:gout_index_srcs",
"//src:gout_srcs",
"//third_party/utest:utest_headers",
]
diff --git a/build/BUILDCONFIG.gn b/build/BUILDCONFIG.gn
@@ -97,7 +97,7 @@ if (is_debug) {
_shared_binary_target_configs += [ "//build:symbols" ]
} else {
_shared_binary_target_configs += [ "//build:release" ]
- _shared_binary_target_configs += [ "//build:optimize" ]
+ _shared_binary_target_configs += [ "//build:optimize_size" ]
_shared_binary_target_configs += [ "//build:no_symbols" ]
}
diff --git a/src/BUILD.gn b/src/BUILD.gn
@@ -42,6 +42,8 @@ source_set("security") {
source_set("utils") {
sources = [
+ "fs_posix.c",
+ "fs_posix.h",
"utils.c",
"utils.h",
]
diff --git a/src/format_tests.c b/src/format_tests.c
@@ -264,7 +264,9 @@ UTEST(print_xml_encoded_len, ControlCharacters) {
/* Tab (\t), LF (\n), CR (\r) should be preserved.
* Other control characters like \x01 (SOH) should be filtered. */
- const char* test_str = "a\t\n\r\x01" "b";
+ const char* test_str =
+ "a\t\n\r\x01"
+ "b";
print_xml_encoded_len(out, test_str, -1, true);
fclose(out);
diff --git a/src/fs_inmemory.c b/src/fs_inmemory.c
@@ -0,0 +1,196 @@
+#include "fs_inmemory.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "utils.h"
+
+typedef struct {
+ char* path;
+ char* buf;
+ size_t size;
+} InMemoryFile;
+
+#define MAX_MOCK_FILES 1024
+static InMemoryFile g_files[MAX_MOCK_FILES];
+static size_t g_files_count = 0;
+
+static FILE* g_active_streams[MAX_MOCK_FILES];
+static size_t g_active_streams_count = 0;
+
+static void register_active_stream(FILE* f) {
+ if (g_active_streams_count < MAX_MOCK_FILES) {
+ g_active_streams[g_active_streams_count++] = f;
+ }
+}
+
+static int inmemory_mkdir(const char* path, mode_t mode) {
+ (void)path;
+ (void)mode;
+ return 0;
+}
+
+static int inmemory_mkdirp(const char* path) {
+ (void)path;
+ return 0;
+}
+
+static FILE* inmemory_fopen(const char* path, const char* mode) {
+ if (strcmp(mode, "w") == 0) {
+ if (g_files_count >= MAX_MOCK_FILES) {
+ return NULL;
+ }
+ // Register a new file.
+ InMemoryFile* file = &g_files[g_files_count++];
+ file->path = estrdup(path);
+ file->buf = NULL;
+ file->size = 0;
+ FILE* f = open_memstream(&file->buf, &file->size);
+ register_active_stream(f);
+ return f;
+ }
+ if (strcmp(mode, "r") == 0) {
+ for (size_t i = 0; i < g_files_count; i++) {
+ if (strcmp(g_files[i].path, path) == 0) {
+ if (g_files[i].buf) {
+ FILE* f = fmemopen(g_files[i].buf, strlen(g_files[i].buf), "r");
+ register_active_stream(f);
+ return f;
+ }
+ }
+ }
+ }
+ return NULL;
+}
+
+static int inmemory_fclose(FILE* stream) {
+ for (size_t i = 0; i < g_active_streams_count; i++) {
+ if (g_active_streams[i] == stream) {
+ // Remove from active streams by shifting the rest down.
+ if (i < g_active_streams_count - 1) {
+ memmove(&g_active_streams[i], &g_active_streams[i + 1],
+ (g_active_streams_count - i - 1) * sizeof(FILE*));
+ }
+ g_active_streams_count--;
+ return fclose(stream);
+ }
+ }
+ return fclose(stream); // Fallback for untracked streams.
+}
+
+const char* inmemory_fs_get_buffer(const char* path) {
+ // We MUST flush all active streams to ensure buffers are up to date.
+ for (size_t i = 0; i < g_active_streams_count; i++) {
+ fflush(g_active_streams[i]);
+ }
+ for (size_t i = 0; i < g_files_count; i++) {
+ if (strcmp(g_files[i].path, path) == 0) {
+ return g_files[i].buf;
+ }
+ }
+ return NULL;
+}
+
+void inmemory_fs_clear(void) {
+ for (size_t i = 0; i < g_active_streams_count; i++) {
+ fclose(g_active_streams[i]);
+ }
+ g_active_streams_count = 0;
+
+ for (size_t i = 0; i < g_files_count; i++) {
+ free(g_files[i].path);
+ free(g_files[i].buf);
+ g_files[i].path = NULL;
+ g_files[i].buf = NULL;
+ g_files[i].size = 0;
+ }
+ g_files_count = 0;
+}
+
+static int inmemory_mkstemp(char* template) {
+ // We MUST return a real file descriptor because the code calls fdopen().
+ return mkstemp(template);
+}
+
+static int inmemory_rename(const char* oldpath, const char* newpath) {
+ // If oldpath is a real file on disk (from mkstemp), we read it into our
+ // in-memory registry and then delete the disk file.
+ if (access(oldpath, F_OK) == 0) {
+ FILE* f = fopen(oldpath, "r");
+ if (f) {
+ fseek(f, 0, SEEK_END);
+ long fsize = ftell(f);
+ fseek(f, 0, SEEK_SET);
+
+ char* content = malloc(fsize + 1);
+ fread(content, 1, fsize, f);
+ content[fsize] = '\0';
+ fclose(f);
+
+ // Register/Update the path in our in-memory map.
+ bool found = false;
+ for (size_t i = 0; i < g_files_count; i++) {
+ if (strcmp(g_files[i].path, newpath) == 0) {
+ free(g_files[i].buf);
+ g_files[i].buf = content;
+ g_files[i].size = fsize;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ if (g_files_count < MAX_MOCK_FILES) {
+ InMemoryFile* file = &g_files[g_files_count++];
+ file->path = estrdup(newpath);
+ file->buf = content;
+ file->size = fsize;
+ }
+ }
+ unlink(oldpath);
+ return 0;
+ }
+ }
+ return 0;
+}
+
+static int inmemory_chmod(const char* path, mode_t mode) {
+ (void)path;
+ (void)mode;
+ return 0;
+}
+
+static int inmemory_access(const char* path, int amode) {
+ (void)amode;
+ for (size_t i = 0; i < g_files_count; i++) {
+ if (strcmp(g_files[i].path, path) == 0) {
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static char* inmemory_realpath(const char* path, char* resolved_path) {
+ if (resolved_path) {
+ strcpy(resolved_path, path);
+ return resolved_path;
+ }
+ return strdup(path);
+}
+
+static const FileSystem kStdInmemoryFs = {
+
+ .mkdir = inmemory_mkdir,
+ .mkdirp = inmemory_mkdirp,
+ .fopen = inmemory_fopen,
+ .fclose = inmemory_fclose,
+ .mkstemp = inmemory_mkstemp,
+ .rename = inmemory_rename,
+ .chmod = inmemory_chmod,
+ .access = inmemory_access,
+ .realpath = inmemory_realpath,
+};
+
+const FileSystem* g_fs_inmemory = &kStdInmemoryFs;
diff --git a/src/fs_inmemory.h b/src/fs_inmemory.h
@@ -0,0 +1,18 @@
+#ifndef GOUT_FS_INMEMORY_H_
+#define GOUT_FS_INMEMORY_H_
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+#include "utils.h"
+
+/* Global in-memory file system implementation. */
+extern const FileSystem* g_fs_inmemory;
+
+/* Returns the buffer for the specified path, or NULL if not found. */
+const char* inmemory_fs_get_buffer(const char* path);
+
+/* Clears all memory buffers in the mock filesystem. */
+void inmemory_fs_clear(void);
+
+#endif // GOUT_FS_INMEMORY_H_
diff --git a/src/fs_posix.c b/src/fs_posix.c
@@ -0,0 +1,80 @@
+#include "fs_posix.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "utils.h"
+
+static int posix_mkdir(const char* path, mode_t mode) {
+ return mkdir(path, mode);
+}
+
+static int posix_mkdirp(const char* path) {
+ char* mut_path = estrdup(path);
+
+ for (char* p = mut_path + (mut_path[0] == '/'); *p; p++) {
+ if (*p != '/') {
+ continue;
+ }
+ *p = '\0';
+ if (mkdir(mut_path, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) {
+ free(mut_path);
+ return -1;
+ }
+ *p = '/';
+ }
+ if (mkdir(mut_path, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) {
+ free(mut_path);
+ return -1;
+ }
+
+ free(mut_path);
+ return 0;
+}
+
+static FILE* posix_fopen(const char* path, const char* mode) {
+ return fopen(path, mode);
+}
+
+static int posix_fclose(FILE* stream) {
+ return fclose(stream);
+}
+
+static int posix_mkstemp(char* template) {
+ return mkstemp(template);
+}
+
+static int posix_rename(const char* oldpath, const char* newpath) {
+ return rename(oldpath, newpath);
+}
+
+static int posix_chmod(const char* path, mode_t mode) {
+ return chmod(path, mode);
+}
+
+static int posix_access(const char* path, int amode) {
+ return access(path, amode);
+}
+
+static char* posix_realpath(const char* path, char* resolved_path) {
+ return realpath(path, resolved_path);
+}
+
+static const FileSystem kStdPosixFs = {
+
+ .mkdir = posix_mkdir,
+ .mkdirp = posix_mkdirp,
+ .fopen = posix_fopen,
+ .fclose = posix_fclose,
+ .mkstemp = posix_mkstemp,
+ .rename = posix_rename,
+ .chmod = posix_chmod,
+ .access = posix_access,
+ .realpath = posix_realpath,
+};
+
+const FileSystem* g_fs_posix = &kStdPosixFs;
diff --git a/src/fs_posix.h b/src/fs_posix.h
@@ -0,0 +1,9 @@
+#ifndef GOUT_FS_POSIX_H_
+#define GOUT_FS_POSIX_H_
+
+#include "utils.h"
+
+/* POSIX file system implementation. */
+extern const FileSystem* g_fs_posix;
+
+#endif // GOUT_FS_POSIX_H_
diff --git a/src/git/commit.c b/src/git/commit.c
@@ -1,6 +1,5 @@
#include "git/commit.h"
-#include <assert.h>
#include <err.h>
#include <git2/commit.h>
#include <git2/diff.h>
diff --git a/src/git/delta.c b/src/git/delta.c
@@ -1,6 +1,5 @@
#include "git/delta.h"
-#include <assert.h>
#include <git2/diff.h>
#include <git2/patch.h>
#include <stdlib.h>
@@ -26,9 +25,9 @@ static char* gitdelta_graph(const GitDelta* delta,
char character,
size_t max_width);
-GitHunkLine* githunkline_create(git_patch* patch,
- size_t hunk_id,
- size_t line_id) {
+static GitHunkLine* githunkline_create(git_patch* patch,
+ size_t hunk_id,
+ size_t line_id) {
const git_diff_line* line;
if (git_patch_get_line_in_hunk(&line, patch, hunk_id, line_id)) {
return NULL;
@@ -44,7 +43,7 @@ GitHunkLine* githunkline_create(git_patch* patch,
return line_out;
}
-void githunkline_free(GitHunkLine* hunk_line) {
+static void githunkline_free(GitHunkLine* hunk_line) {
if (!hunk_line) {
return;
}
@@ -52,7 +51,7 @@ void githunkline_free(GitHunkLine* hunk_line) {
free(hunk_line);
}
-GitHunk* githunk_create(git_patch* patch, size_t hunk_id) {
+static GitHunk* githunk_create(git_patch* patch, size_t hunk_id) {
const git_diff_hunk* hunk;
size_t line_count;
if (git_patch_get_hunk(&hunk, &line_count, patch, hunk_id)) {
@@ -74,7 +73,7 @@ GitHunk* githunk_create(git_patch* patch, size_t hunk_id) {
return hunk_out;
}
-void githunk_free(GitHunk* hunk) {
+static void githunk_free(GitHunk* hunk) {
if (!hunk) {
return;
}
@@ -88,7 +87,7 @@ void githunk_free(GitHunk* hunk) {
free(hunk);
}
-char status_for_delta(const git_diff_delta* delta) {
+static char status_for_delta(const git_diff_delta* delta) {
switch (delta->status) {
case GIT_DELTA_ADDED:
return 'A';
@@ -178,10 +177,10 @@ void gitdelta_free(GitDelta* delta) {
free(delta);
}
-char* gitdelta_graph(const GitDelta* delta,
- size_t length,
- char c,
- size_t max_width) {
+static char* gitdelta_graph(const GitDelta* delta,
+ size_t length,
+ char c,
+ size_t max_width) {
size_t changed = delta->addcount + delta->delcount;
if (changed > max_width && length > 0) {
length = (length * max_width) / changed;
diff --git a/src/git/git.c b/src/git/git.c
@@ -1,14 +1,53 @@
#include "git.h"
-#include <git2/common.h>
-#include <git2/config.h>
-#include <git2/global.h>
-#include <git2/oid.h>
+#include <err.h>
+#include <git2.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#include "git/internal.h"
+#include "third_party/openbsd/reallocarray.h"
+#include "utils.h"
/* Global const data. */
const size_t kOidLen = GIT_OID_SHA1_HEXSIZE + 1;
-void gout_git_initialize(void) {
+/* Maximum file size to load into memory, in bytes. */
+static const ssize_t kMaxFileSizeBytes = 16 * 1024 * 1024;
+
+/* Forward declarations for VTable functions. */
+static void libgit2_initialize(Git* git);
+static void libgit2_shutdown(Git* git);
+static void libgit2_for_repo(Git* git,
+ const char* path,
+ RepoCallback cb,
+ void* user_data);
+static void libgit2_for_each_commit(Git* git,
+ CommitCallback cb,
+ void* user_data);
+static void libgit2_for_commit(Git* git,
+ const char* spec,
+ CommitCallback cb,
+ void* user_data);
+static void libgit2_for_each_reference(Git* git,
+ ReferenceCallback cb,
+ void* user_data);
+static void libgit2_for_each_file(Git* git, FileCallback cb, void* user_data);
+
+/* Internal libgit2 utilities. */
+static int oid_for_spec(git_repository* repo, const char* spec, git_oid* oid);
+static size_t string_count_lines(const char* str, ssize_t str_len);
+static char get_filetype(git_filemode_t m);
+static char* format_filemode(git_filemode_t m);
+static bool gitrepo_walk_tree_files(git_repository* repo,
+ git_tree* tree,
+ const char* path,
+ FileCallback cb,
+ void* user_data);
+
+static void libgit2_initialize(Git* git) {
+ (void)git;
/* do not search outside the git repository:
GIT_CONFIG_LEVEL_APP is the highest level currently */
git_libgit2_init();
@@ -19,6 +58,331 @@ void gout_git_initialize(void) {
git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
}
-void gout_git_shutdown(void) {
+static void libgit2_shutdown(Git* git) {
+ (void)git;
git_libgit2_shutdown();
}
+
+Git* gout_git_create(const FileSystem* fs) {
+ Git* git = ecalloc(1, sizeof(Git));
+ git->impl = NULL;
+ git->fs = fs;
+ git->repo = NULL;
+
+ git->initialize = libgit2_initialize;
+ git->shutdown = libgit2_shutdown;
+ git->for_repo = libgit2_for_repo;
+ git->for_each_commit = libgit2_for_each_commit;
+ git->for_commit = libgit2_for_commit;
+ git->for_each_reference = libgit2_for_each_reference;
+ git->for_each_file = libgit2_for_each_file;
+
+ return git;
+}
+
+void gout_git_free(Git* git) {
+ if (!git) {
+ return;
+ }
+ if (git->impl) {
+ git_repository_free((git_repository*)git->impl);
+ }
+ if (git->repo) {
+ gitrepo_free(git->repo);
+ }
+ free(git);
+}
+
+/* VTable Implementation */
+
+static void libgit2_for_repo(Git* git,
+ const char* path,
+ RepoCallback cb,
+ void* user_data) {
+ git->impl = gitrepo_open_repository(path);
+ git->repo = gitrepo_create_from_repository(git->fs, path, git->impl);
+ cb(git, user_data);
+ gitrepo_free(git->repo);
+ git->repo = NULL;
+ git_repository_free((git_repository*)git->impl);
+ git->impl = NULL;
+}
+
+static void libgit2_for_each_commit(Git* git,
+ CommitCallback cb,
+ void* user_data) {
+ git_repository* repo = (git_repository*)git->impl;
+ git_oid start_oid;
+ if (oid_for_spec(repo, "HEAD", &start_oid) != 0) {
+ return;
+ }
+
+ git_revwalk* revwalk = NULL;
+ if (git_revwalk_new(&revwalk, repo) != 0) {
+ errx(1, "git_revwalk_new");
+ }
+ git_revwalk_push(revwalk, &start_oid);
+
+ git_oid current;
+ while (!git_revwalk_next(¤t, revwalk)) {
+ GitCommit* commit = gitcommit_create(¤t, repo);
+ cb(commit, user_data);
+ gitcommit_free(commit);
+ }
+ git_revwalk_free(revwalk);
+}
+
+static void libgit2_for_commit(Git* git,
+ const char* spec,
+ CommitCallback cb,
+ void* user_data) {
+ git_repository* repo = (git_repository*)git->impl;
+ git_oid start_oid;
+ if (oid_for_spec(repo, spec, &start_oid) != 0) {
+ return;
+ }
+
+ git_revwalk* revwalk = NULL;
+ if (git_revwalk_new(&revwalk, repo) != 0) {
+ errx(1, "git_revwalk_new");
+ }
+ git_revwalk_push(revwalk, &start_oid);
+
+ git_oid current;
+ if (!git_revwalk_next(¤t, revwalk)) {
+ GitCommit* commit = gitcommit_create(¤t, repo);
+ cb(commit, user_data);
+ gitcommit_free(commit);
+ }
+ git_revwalk_free(revwalk);
+}
+
+static void libgit2_for_each_reference(Git* git,
+ ReferenceCallback cb,
+ void* user_data) {
+ git_repository* repo = (git_repository*)git->impl;
+ git_reference_iterator* it = NULL;
+ if (git_reference_iterator_new(&it, repo) != 0) {
+ errx(1, "git_reference_iterator_new");
+ }
+
+ GitReference** repos = NULL;
+ size_t repos_len = 0;
+ git_reference* current = NULL;
+ while (!git_reference_next(¤t, it)) {
+ if (!git_reference_is_branch(current) && !git_reference_is_tag(current)) {
+ git_reference_free(current);
+ continue;
+ }
+ GitReference* ref = gitreference_create(repo, current);
+ repos = reallocarray(repos, repos_len + 1, sizeof(GitReference*));
+ if (!repos) {
+ err(1, "reallocarray");
+ }
+ repos[repos_len++] = ref;
+ }
+ git_reference_iterator_free(it);
+ if (repos_len > 0) {
+ qsort(repos, repos_len, sizeof(GitReference*), gitreference_compare);
+ }
+
+ for (size_t i = 0; i < repos_len; i++) {
+ cb(repos[i], user_data);
+ gitreference_free(repos[i]);
+ }
+ free(repos);
+}
+
+static void libgit2_for_each_file(Git* git, FileCallback cb, void* user_data) {
+ git_repository* repo = (git_repository*)git->impl;
+ git_commit* commit = NULL;
+ git_oid id;
+ if (oid_for_spec(repo, "HEAD", &id) != 0 ||
+ git_commit_lookup(&commit, repo, &id) != 0) {
+ return;
+ }
+ git_tree* tree = NULL;
+ if (git_commit_tree(&tree, commit) != 0) {
+ git_commit_free(commit);
+ return;
+ }
+ git_commit_free(commit);
+
+ if (!gitrepo_walk_tree_files(repo, tree, "", cb, user_data)) {
+ git_tree_free(tree);
+ return;
+ }
+ git_tree_free(tree);
+}
+
+/* Utilities */
+
+static int oid_for_spec(git_repository* repo, const char* spec, git_oid* oid) {
+ git_object* obj = NULL;
+ if (git_revparse_single(&obj, repo, spec)) {
+ return -1;
+ }
+ git_oid_cpy(oid, git_object_id(obj));
+ git_object_free(obj);
+ return 0;
+}
+
+static size_t string_count_lines(const char* str, ssize_t str_len) {
+ if (str_len <= 0) {
+ return 0;
+ }
+ size_t lines = 0;
+ for (ssize_t i = 0; i < str_len; i++) {
+ if (str[i] == '\n') {
+ lines++;
+ }
+ }
+ return str[str_len - 1] == '\n' ? lines : lines + 1;
+}
+
+static char get_filetype(git_filemode_t m) {
+ switch (m & S_IFMT) {
+ case S_IFREG:
+ return '-';
+ case S_IFBLK:
+ return 'b';
+ case S_IFCHR:
+ return 'c';
+ case S_IFDIR:
+ return 'd';
+ case S_IFIFO:
+ return 'p';
+ case S_IFLNK:
+ return 'l';
+ case S_IFSOCK:
+ return 's';
+ default:
+ return '?';
+ }
+}
+
+static char* format_filemode(git_filemode_t m) {
+ char* mode = ecalloc(11, sizeof(char));
+ memset(mode, '-', 10);
+ mode[10] = '\0';
+
+ mode[0] = get_filetype(m);
+ if (m & S_IRUSR) {
+ mode[1] = 'r';
+ }
+ if (m & S_IWUSR) {
+ mode[2] = 'w';
+ }
+ if (m & S_IXUSR) {
+ mode[3] = 'x';
+ }
+ if (m & S_IRGRP) {
+ mode[4] = 'r';
+ }
+ if (m & S_IWGRP) {
+ mode[5] = 'w';
+ }
+ if (m & S_IXGRP) {
+ mode[6] = 'x';
+ }
+ if (m & S_IROTH) {
+ mode[7] = 'r';
+ }
+ if (m & S_IWOTH) {
+ mode[8] = 'w';
+ }
+ if (m & S_IXOTH) {
+ mode[9] = 'x';
+ }
+
+ if (m & S_ISUID) {
+ mode[3] = (mode[3] == 'x') ? 's' : 'S';
+ }
+ if (m & S_ISGID) {
+ mode[6] = (mode[6] == 'x') ? 's' : 'S';
+ }
+ if (m & S_ISVTX) {
+ mode[9] = (mode[9] == 'x') ? 't' : 'T';
+ }
+
+ return mode;
+}
+
+static bool gitrepo_walk_tree_files(git_repository* repo,
+ git_tree* tree,
+ const char* path,
+ FileCallback cb,
+ void* user_data) {
+ for (size_t i = 0; i < git_tree_entrycount(tree); i++) {
+ const git_tree_entry* entry = git_tree_entry_byindex(tree, i);
+ if (!entry) {
+ return false;
+ }
+
+ const char* entryname = git_tree_entry_name(entry);
+ if (!entryname) {
+ return false;
+ }
+
+ char* entrypath;
+ if (path[0] == '\0') {
+ entrypath = estrdup(entryname);
+ } else {
+ entrypath = path_concat(path, entryname);
+ }
+
+ git_object* obj = NULL;
+ if (git_tree_entry_to_object(&obj, repo, entry) != 0) {
+ char oid_str[GIT_OID_SHA1_HEXSIZE + 1];
+ git_oid_tostr(oid_str, sizeof(oid_str), git_tree_entry_id(entry));
+ GitFile* fileinfo =
+ gitfile_create(kFileTypeSubmodule, "m---------", entrypath,
+ ".gitmodules", oid_str, -1, -1, "");
+ cb(fileinfo, user_data);
+ gitfile_free(fileinfo);
+ } else {
+ switch (git_object_type(obj)) {
+ case GIT_OBJECT_BLOB:
+ break;
+ case GIT_OBJECT_TREE: {
+ if (!gitrepo_walk_tree_files(repo, (git_tree*)obj, entrypath, cb,
+ user_data)) {
+ git_object_free(obj);
+ free(entrypath);
+ return false;
+ }
+ git_object_free(obj);
+ free(entrypath);
+ continue;
+ }
+ default:
+ git_object_free(obj);
+ free(entrypath);
+ continue;
+ }
+
+ git_blob* blob = (git_blob*)obj;
+ ssize_t size_bytes = git_blob_rawsize(blob);
+ ssize_t size_lines = -1;
+ const char* content = "";
+
+ if (size_bytes > kMaxFileSizeBytes) {
+ size_lines = -2; /* oversized file */
+ } else if (!git_blob_is_binary(blob)) {
+ content = (const char*)git_blob_rawcontent(blob);
+ size_lines = string_count_lines(content, size_bytes);
+ }
+
+ char* filemode = format_filemode(git_tree_entry_filemode(entry));
+ GitFile* fileinfo =
+ gitfile_create(kFileTypeFile, filemode, entrypath, entrypath, "",
+ size_bytes, size_lines, content);
+ cb(fileinfo, user_data);
+ gitfile_free(fileinfo);
+ git_object_free(obj);
+ free(filemode);
+ }
+ free(entrypath);
+ }
+ return true;
+}
diff --git a/src/git/git.h b/src/git/git.h
@@ -3,9 +3,43 @@
#include <stddef.h>
+#include "git/commit.h"
+#include "git/file.h"
+#include "git/reference.h"
+#include "git/repo.h"
+#include "utils.h"
+
extern const size_t kOidLen;
-void gout_git_initialize(void);
-void gout_git_shutdown(void);
+typedef struct Git Git;
+
+typedef void (*RepoCallback)(Git* git, void* user_data);
+typedef void (*CommitCallback)(const GitCommit* ci, void* user_data);
+typedef void (*ReferenceCallback)(const GitReference* ref, void* user_data);
+typedef void (*FileCallback)(const GitFile* file, void* user_data);
+
+struct Git {
+ void* impl;
+ GitRepo* repo;
+ const FileSystem* fs;
+
+ void (*initialize)(Git* git);
+ void (*shutdown)(Git* git);
+
+ void (*for_repo)(Git* git,
+ const char* path,
+ RepoCallback cb,
+ void* user_data);
+ void (*for_each_commit)(Git* git, CommitCallback cb, void* user_data);
+ void (*for_commit)(Git* git,
+ const char* spec,
+ CommitCallback cb,
+ void* user_data);
+ void (*for_each_reference)(Git* git, ReferenceCallback cb, void* user_data);
+ void (*for_each_file)(Git* git, FileCallback cb, void* user_data);
+};
+
+Git* gout_git_create(const FileSystem* fs);
+void gout_git_free(Git* git);
#endif // GOUT_GIT_GIT_H_
diff --git a/src/git/internal.h b/src/git/internal.h
@@ -28,4 +28,15 @@ GitReference* gitreference_create(git_repository* repo, git_reference* ref);
void gitreference_free(GitReference* ref);
int gitreference_compare(const void* r1, const void* r2);
+#include "git/repo.h"
+#include "utils.h"
+void gitrepo_load_filesystem_metadata(GitRepo* repo,
+ const FileSystem* fs,
+ const char* path);
+GitRepo* gitrepo_create_from_repository(const FileSystem* fs,
+ const char* path,
+ void* git_repo);
+void gitrepo_free(GitRepo* repo);
+git_repository* gitrepo_open_repository(const char* path);
+
#endif // GOUT_GIT_INTERNAL_H_
diff --git a/src/git/repo.c b/src/git/repo.c
@@ -23,22 +23,8 @@
#include <unistd.h>
#include "git/internal.h"
-#include "third_party/openbsd/reallocarray.h"
#include "utils.h"
-struct GitRepo {
- char* name;
- char* short_name;
- char* owner;
- char* description;
- char* clone_url;
- char* submodules;
- char* readme;
- char* license;
-
- git_repository* repo;
-};
-
/* Local const data */
static const char* kLicenses[] = {"HEAD:LICENSE", "HEAD:LICENSE.md",
"HEAD:COPYING"};
@@ -46,16 +32,9 @@ static const size_t kLicensesLen = sizeof(kLicenses) / sizeof(char*);
static const char* kReadmes[] = {"HEAD:README", "HEAD:README.md"};
static const size_t kReadmesLen = sizeof(kReadmes) / sizeof(char*);
-/* Maximum file size to load into memory, in bytes. */
-static const ssize_t kMaxFileSizeBytes = 16 * 1024 * 1024;
-
/* Utilities */
-static size_t string_count_lines(const char* str, ssize_t size_bytes);
static bool string_ends_with(const char* str, const char* suffix);
-static char* first_line_of_file(const char* path);
-static const git_oid* oid_for_spec(git_repository* repo, const char* spec);
-static char get_filetype(git_filemode_t m);
-static char* format_filemode(git_filemode_t m);
+static char* first_line_of_file(const FileSystem* fs, const char* path);
/* GitRepo utilities. */
static char* gitrepo_name_from_path(const char* repo_path);
@@ -64,27 +43,12 @@ static bool gitrepo_has_blob(git_repository* repo, const char* file);
static const char* gitrepo_first_matching_file(git_repository* repo,
const char** files,
size_t files_len);
-static void gitrepo_load_metadata(GitRepo* repo, const char* path);
-static bool gitrepo_walk_tree_files(git_repository* repo,
- git_tree* tree,
- const char* path,
- FileCallback cb,
- void* user_data);
-
-size_t string_count_lines(const char* str, ssize_t str_len) {
- if (str_len <= 0) {
- return 0;
- }
- size_t lines = 0;
- for (ssize_t i = 0; i < str_len; i++) {
- if (str[i] == '\n') {
- lines++;
- }
- }
- return str[str_len - 1] == '\n' ? lines : lines + 1;
-}
+void gitrepo_load_filesystem_metadata(GitRepo* repo,
+ const FileSystem* fs,
+ const char* path);
+static void gitrepo_load_git_metadata(GitRepo* repo, git_repository* git_repo);
-bool string_ends_with(const char* str, const char* suffix) {
+static bool string_ends_with(const char* str, const char* suffix) {
if (!str || !suffix) {
return false;
}
@@ -96,15 +60,15 @@ bool string_ends_with(const char* str, const char* suffix) {
return strncmp(str + str_len - suffix_len, suffix, suffix_len) == 0;
}
-char* first_line_of_file(const char* path) {
- FILE* f = fopen(path, "r");
+static char* first_line_of_file(const FileSystem* fs, const char* path) {
+ FILE* f = fs->fopen(path, "r");
if (!f) {
return estrdup("");
}
char* buf = NULL;
size_t buf_size = 0;
ssize_t len = getline(&buf, &buf_size, f);
- fclose(f);
+ fs->fclose(f);
if (len == -1) {
free(buf);
return estrdup("");
@@ -116,85 +80,7 @@ char* first_line_of_file(const char* path) {
return buf;
}
-const git_oid* oid_for_spec(git_repository* repo, const char* spec) {
- git_object* obj = NULL;
- if (git_revparse_single(&obj, repo, spec)) {
- return NULL;
- }
- const git_oid* oid = git_object_id(obj);
- git_object_free(obj);
- return oid;
-}
-
-char get_filetype(git_filemode_t m) {
- switch (m & S_IFMT) {
- case S_IFREG:
- return '-';
- case S_IFBLK:
- return 'b';
- case S_IFCHR:
- return 'c';
- case S_IFDIR:
- return 'd';
- case S_IFIFO:
- return 'p';
- case S_IFLNK:
- return 'l';
- case S_IFSOCK:
- return 's';
- default:
- return '?';
- }
-}
-
-char* format_filemode(git_filemode_t m) {
- char* mode = ecalloc(11, sizeof(char));
- memset(mode, '-', 10);
- mode[10] = '\0';
-
- mode[0] = get_filetype(m);
- if (m & S_IRUSR) {
- mode[1] = 'r';
- }
- if (m & S_IWUSR) {
- mode[2] = 'w';
- }
- if (m & S_IXUSR) {
- mode[3] = 'x';
- }
- if (m & S_IRGRP) {
- mode[4] = 'r';
- }
- if (m & S_IWGRP) {
- mode[5] = 'w';
- }
- if (m & S_IXGRP) {
- mode[6] = 'x';
- }
- if (m & S_IROTH) {
- mode[7] = 'r';
- }
- if (m & S_IWOTH) {
- mode[8] = 'w';
- }
- if (m & S_IXOTH) {
- mode[9] = 'x';
- }
-
- if (m & S_ISUID) {
- mode[3] = (mode[3] == 'x') ? 's' : 'S';
- }
- if (m & S_ISGID) {
- mode[6] = (mode[6] == 'x') ? 's' : 'S';
- }
- if (m & S_ISVTX) {
- mode[9] = (mode[9] == 'x') ? 't' : 'T';
- }
-
- return mode;
-}
-
-char* gitrepo_name_from_path(const char* repo_path) {
+static char* gitrepo_name_from_path(const char* repo_path) {
char* path_copy = estrdup(repo_path);
const char* filename = basename(path_copy);
if (!filename) {
@@ -205,7 +91,7 @@ char* gitrepo_name_from_path(const char* repo_path) {
return result;
}
-char* gitrepo_shortname_from_name(const char* name) {
+static char* gitrepo_shortname_from_name(const char* name) {
char* short_name = estrdup(name);
if (string_ends_with(short_name, ".git")) {
size_t short_name_len = strlen(short_name);
@@ -215,7 +101,7 @@ char* gitrepo_shortname_from_name(const char* name) {
return short_name;
}
-bool gitrepo_has_blob(git_repository* repo, const char* file) {
+static bool gitrepo_has_blob(git_repository* repo, const char* file) {
git_object* obj = NULL;
if (git_revparse_single(&obj, repo, file)) {
return false;
@@ -225,9 +111,9 @@ bool gitrepo_has_blob(git_repository* repo, const char* file) {
return has_blob;
}
-const char* gitrepo_first_matching_file(git_repository* repo,
- const char** files,
- size_t files_len) {
+static const char* gitrepo_first_matching_file(git_repository* repo,
+ const char** files,
+ size_t files_len) {
for (size_t i = 0; i < files_len; i++) {
const char* filename = files[i];
if (gitrepo_has_blob(repo, filename)) {
@@ -237,15 +123,17 @@ const char* gitrepo_first_matching_file(git_repository* repo,
return "";
}
-void gitrepo_load_metadata(GitRepo* repo, const char* path) {
- char* repo_path = realpath(path, NULL);
+void gitrepo_load_filesystem_metadata(GitRepo* repo,
+ const FileSystem* fs,
+ const char* path) {
+ char* repo_path = fs->realpath(path, NULL);
if (!repo_path) {
- err(1, "realpath");
+ err(1, "realpath: %s", path);
}
char* git_path_maybe = path_concat(repo_path, ".git");
char* git_path = NULL;
- if (access(git_path_maybe, F_OK) != 0) {
+ if (fs->access(git_path_maybe, F_OK) != 0) {
git_path = estrdup(repo_path);
} else {
git_path = estrdup(git_path_maybe);
@@ -258,15 +146,9 @@ void gitrepo_load_metadata(GitRepo* repo, const char* path) {
repo->name = gitrepo_name_from_path(repo_path);
repo->short_name = gitrepo_shortname_from_name(repo->name);
- repo->owner = first_line_of_file(owner_path);
- repo->description = first_line_of_file(desc_path);
- repo->clone_url = first_line_of_file(url_path);
- repo->submodules = estrdup(
- gitrepo_has_blob(repo->repo, "HEAD:.gitmodules") ? ".gitmodules" : "");
- repo->readme =
- estrdup(gitrepo_first_matching_file(repo->repo, kReadmes, kReadmesLen));
- repo->license =
- estrdup(gitrepo_first_matching_file(repo->repo, kLicenses, kLicensesLen));
+ repo->owner = first_line_of_file(fs, owner_path);
+ repo->description = first_line_of_file(fs, desc_path);
+ repo->clone_url = first_line_of_file(fs, url_path);
free(repo_path);
free(git_path);
@@ -275,16 +157,41 @@ void gitrepo_load_metadata(GitRepo* repo, const char* path) {
free(url_path);
}
-GitRepo* gitrepo_create(const char* path) {
- GitRepo* repo = ecalloc(1, sizeof(GitRepo));
- git_repository* git_repo = NULL;
+static void gitrepo_load_git_metadata(GitRepo* repo, git_repository* git_repo) {
+ repo->submodules = estrdup(
+ gitrepo_has_blob(git_repo, "HEAD:.gitmodules") ? ".gitmodules" : "");
+ repo->readme =
+ estrdup(gitrepo_first_matching_file(git_repo, kReadmes, kReadmesLen));
+ repo->license =
+ estrdup(gitrepo_first_matching_file(git_repo, kLicenses, kLicensesLen));
+
+ repo->last_commit_time = 0;
+ git_object* obj = NULL;
+ if (git_revparse_single(&obj, git_repo, "HEAD") == 0) {
+ git_commit* commit = NULL;
+ if (git_commit_lookup(&commit, git_repo, git_object_id(obj)) == 0) {
+ repo->last_commit_time = git_commit_author(commit)->when.time;
+ git_commit_free(commit);
+ }
+ git_object_free(obj);
+ }
+}
+
+git_repository* gitrepo_open_repository(const char* path) {
+ git_repository* repo = NULL;
git_repository_open_flag_t kRepoOpenFlags = GIT_REPOSITORY_OPEN_NO_SEARCH;
- if (git_repository_open_ext(&git_repo, path, kRepoOpenFlags, NULL)) {
+ if (git_repository_open_ext(&repo, path, kRepoOpenFlags, NULL)) {
errx(1, "invalid git repository: %s", path);
}
- repo->repo = git_repo;
+ return repo;
+}
- gitrepo_load_metadata(repo, path);
+GitRepo* gitrepo_create_from_repository(const FileSystem* fs,
+ const char* path,
+ void* git_repo) {
+ GitRepo* repo = ecalloc(1, sizeof(GitRepo));
+ gitrepo_load_filesystem_metadata(repo, fs, path);
+ gitrepo_load_git_metadata(repo, (git_repository*)git_repo);
return repo;
}
@@ -308,218 +215,5 @@ void gitrepo_free(GitRepo* repo) {
repo->readme = NULL;
free(repo->license);
repo->license = NULL;
- git_repository_free(repo->repo);
- repo->repo = NULL;
free(repo);
}
-
-const char* gitrepo_name(const GitRepo* repo) {
- return repo->name;
-}
-
-const char* gitrepo_short_name(const GitRepo* repo) {
- return repo->short_name;
-}
-
-const char* gitrepo_owner(const GitRepo* repo) {
- return repo->owner;
-}
-
-const char* gitrepo_description(const GitRepo* repo) {
- return repo->description;
-}
-
-const char* gitrepo_clone_url(const GitRepo* repo) {
- return repo->clone_url;
-}
-
-const char* gitrepo_submodules(const GitRepo* repo) {
- return repo->submodules;
-}
-
-const char* gitrepo_readme(const GitRepo* repo) {
- return repo->readme;
-}
-
-const char* gitrepo_license(const GitRepo* repo) {
- return repo->license;
-}
-
-void gitrepo_for_commit(GitRepo* repo,
- const char* spec,
- CommitCallback cb,
- void* user_data) {
- const git_oid* start_oid = oid_for_spec(repo->repo, spec);
- if (start_oid == NULL) {
- return;
- }
-
- git_revwalk* revwalk = NULL;
- if (git_revwalk_new(&revwalk, repo->repo) != 0) {
- errx(1, "git_revwalk_new");
- }
- git_revwalk_push(revwalk, start_oid);
-
- git_oid current;
- if (!git_revwalk_next(¤t, revwalk)) {
- GitCommit* commit = gitcommit_create(¤t, repo->repo);
- cb(commit, user_data);
- gitcommit_free(commit);
- }
- git_revwalk_free(revwalk);
-}
-
-void gitrepo_for_each_commit(GitRepo* repo,
- CommitCallback cb,
- void* user_data) {
- const git_oid* start_oid = oid_for_spec(repo->repo, "HEAD");
-
- git_revwalk* revwalk = NULL;
- if (git_revwalk_new(&revwalk, repo->repo) != 0) {
- errx(1, "git_revwalk_new");
- }
- git_revwalk_push(revwalk, start_oid);
-
- git_oid current;
- while (!git_revwalk_next(¤t, revwalk)) {
- GitCommit* commit = gitcommit_create(¤t, repo->repo);
- cb(commit, user_data);
- gitcommit_free(commit);
- }
- git_revwalk_free(revwalk);
-}
-
-void gitrepo_for_each_reference(GitRepo* repo,
- ReferenceCallback cb,
- void* user_data) {
- git_reference_iterator* it = NULL;
- if (git_reference_iterator_new(&it, repo->repo) != 0) {
- errx(1, "git_reference_iterator_new");
- }
-
- GitReference** repos = NULL;
- size_t repos_len = 0;
- git_reference* current = NULL;
- while (!git_reference_next(¤t, it)) {
- if (!git_reference_is_branch(current) && !git_reference_is_tag(current)) {
- git_reference_free(current);
- continue;
- }
- // Hand ownership of current to GitReference.
- GitReference* ref = gitreference_create(repo->repo, current);
- repos = reallocarray(repos, repos_len + 1, sizeof(GitReference*));
- if (!repos) {
- err(1, "reallocarray");
- }
- repos[repos_len++] = ref;
- }
- git_reference_iterator_free(it);
- qsort(repos, repos_len, sizeof(GitReference*), gitreference_compare);
-
- for (size_t i = 0; i < repos_len; i++) {
- cb(repos[i], user_data);
- gitreference_free(repos[i]);
- repos[i] = NULL;
- }
- free(repos);
-}
-
-bool gitrepo_walk_tree_files(git_repository* repo,
- git_tree* tree,
- const char* path,
- FileCallback cb,
- void* user_data) {
- for (size_t i = 0; i < git_tree_entrycount(tree); i++) {
- const git_tree_entry* entry = git_tree_entry_byindex(tree, i);
- if (!entry) {
- return false;
- }
-
- const char* entryname = git_tree_entry_name(entry);
- if (!entryname) {
- return false;
- }
-
- char* entrypath;
- if (path[0] == '\0') {
- entrypath = estrdup(entryname);
- } else {
- entrypath = path_concat(path, entryname);
- }
-
- git_object* obj = NULL;
- if (git_tree_entry_to_object(&obj, repo, entry) != 0) {
- char oid_str[GIT_OID_SHA1_HEXSIZE + 1];
- git_oid_tostr(oid_str, sizeof(oid_str), git_tree_entry_id(entry));
- GitFile* fileinfo =
- gitfile_create(kFileTypeSubmodule, "m---------", entrypath,
- ".gitmodules", oid_str, -1, -1, "");
- cb(fileinfo, user_data);
- gitfile_free(fileinfo);
- } else {
- switch (git_object_type(obj)) {
- case GIT_OBJECT_BLOB:
- break;
- case GIT_OBJECT_TREE: {
- /* NOTE: recurses */
- if (!gitrepo_walk_tree_files(repo, (git_tree*)obj, entrypath, cb,
- user_data)) {
- git_object_free(obj);
- free(entrypath);
- return false;
- }
- git_object_free(obj);
- free(entrypath);
- continue;
- }
- default:
- git_object_free(obj);
- free(entrypath);
- continue;
- }
-
- git_blob* blob = (git_blob*)obj;
- ssize_t size_bytes = git_blob_rawsize(blob);
- ssize_t size_lines = -1;
- const char* content = "";
-
- if (size_bytes > kMaxFileSizeBytes) {
- size_lines = -2; /* oversized file */
- } else if (!git_blob_is_binary(blob)) {
- content = (const char*)git_blob_rawcontent(blob);
- size_lines = string_count_lines(content, size_bytes);
- }
-
- char* filemode = format_filemode(git_tree_entry_filemode(entry));
- GitFile* fileinfo =
- gitfile_create(kFileTypeFile, filemode, entrypath, entrypath, "",
- size_bytes, size_lines, content);
- cb(fileinfo, user_data);
- gitfile_free(fileinfo);
- git_object_free(obj);
- free(filemode);
- }
- free(entrypath);
- }
- return true;
-}
-
-void gitrepo_for_each_file(GitRepo* repo, FileCallback cb, void* user_data) {
- git_commit* commit = NULL;
- const git_oid* id = oid_for_spec(repo->repo, "HEAD");
- if (git_commit_lookup(&commit, repo->repo, id) != 0) {
- return;
- }
- git_tree* tree = NULL;
- if (git_commit_tree(&tree, commit) != 0) {
- git_commit_free(commit);
- return;
- }
- git_commit_free(commit);
-
- if (!gitrepo_walk_tree_files(repo->repo, tree, "", cb, user_data)) {
- git_tree_free(tree);
- return;
- }
- git_tree_free(tree);
-}
diff --git a/src/git/repo.h b/src/git/repo.h
@@ -1,38 +1,18 @@
#ifndef GOUT_GIT_REPO_H_
#define GOUT_GIT_REPO_H_
-#include "git/commit.h"
-#include "git/file.h"
-#include "git/reference.h"
-
-typedef struct GitRepo GitRepo;
-
-GitRepo* gitrepo_create(const char* path);
-void gitrepo_free(GitRepo* repo);
-
-const char* gitrepo_name(const GitRepo* repo);
-const char* gitrepo_short_name(const GitRepo* repo);
-const char* gitrepo_owner(const GitRepo* repo);
-const char* gitrepo_description(const GitRepo* repo);
-const char* gitrepo_clone_url(const GitRepo* repo);
-const char* gitrepo_submodules(const GitRepo* repo);
-const char* gitrepo_readme(const GitRepo* repo);
-const char* gitrepo_license(const GitRepo* repo);
-
-typedef void (*CommitCallback)(const GitCommit* ci, void* user_data);
-void gitrepo_for_commit(GitRepo* repo,
- const char* spec,
- CommitCallback cb,
- void* user_data);
-
-void gitrepo_for_each_commit(GitRepo* repo, CommitCallback cb, void* user_data);
-
-typedef void (*ReferenceCallback)(const GitReference* ref, void* user_data);
-void gitrepo_for_each_reference(GitRepo* repo,
- ReferenceCallback cb,
- void* user_data);
-
-typedef void (*FileCallback)(const GitFile* file, void* user_data);
-void gitrepo_for_each_file(GitRepo* repo, FileCallback cb, void* user_data);
+#include <time.h>
+
+typedef struct GitRepo {
+ char* name;
+ char* short_name;
+ char* owner;
+ char* description;
+ char* clone_url;
+ char* submodules;
+ char* readme;
+ char* license;
+ time_t last_commit_time;
+} GitRepo;
#endif // GOUT_GIT_REPO_H_
diff --git a/src/git/repo_tests.c b/src/git/repo_tests.c
@@ -0,0 +1,69 @@
+#include "git/internal.h"
+#include "git/repo.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "utest.h"
+
+struct git_repo {
+ int dummy;
+};
+
+UTEST_F_SETUP(git_repo) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(git_repo) {}
+
+UTEST_F(git_repo, load_filesystem_metadata) {
+ /* 1. Setup mock repo files. */
+ const char* repo_path = "/path/to/my-repo.git";
+
+ /* Mock description */
+ FILE* f_desc = g_fs_inmemory->fopen("/path/to/my-repo.git/description", "w");
+ fprintf(f_desc, "My project description\n");
+ g_fs_inmemory->fclose(f_desc);
+
+ /* Mock owner */
+ FILE* f_owner = g_fs_inmemory->fopen("/path/to/my-repo.git/owner", "w");
+ fprintf(f_owner, "John Doe\n");
+ g_fs_inmemory->fclose(f_owner);
+
+ /* Mock url */
+ FILE* f_url = g_fs_inmemory->fopen("/path/to/my-repo.git/url", "w");
+ fprintf(f_url, "https://example.com/repo.git\n");
+ g_fs_inmemory->fclose(f_url);
+
+ /* 2. Load metadata. */
+ GitRepo* repo = ecalloc(1, sizeof(GitRepo));
+ gitrepo_load_filesystem_metadata(repo, g_fs_inmemory, repo_path);
+
+ /* 3. Verify. */
+ EXPECT_STREQ("my-repo.git", repo->name);
+ EXPECT_STREQ("my-repo", repo->short_name);
+ EXPECT_STREQ("My project description", repo->description);
+ EXPECT_STREQ("John Doe", repo->owner);
+ EXPECT_STREQ("https://example.com/repo.git", repo->clone_url);
+
+ gitrepo_free(repo);
+}
+
+UTEST_F(git_repo, load_filesystem_metadata_bare) {
+ /* Setup a non-dot-git directory (simulating a bare repo or just a dir). */
+ const char* repo_path = "/path/to/my-bare-repo";
+
+ FILE* f_desc = g_fs_inmemory->fopen("/path/to/my-bare-repo/description", "w");
+ fprintf(f_desc, "Bare repo desc\n");
+ g_fs_inmemory->fclose(f_desc);
+
+ GitRepo* repo = ecalloc(1, sizeof(GitRepo));
+ gitrepo_load_filesystem_metadata(repo, g_fs_inmemory, repo_path);
+
+ EXPECT_STREQ("my-bare-repo", repo->name);
+ EXPECT_STREQ("Bare repo desc", repo->description);
+
+ gitrepo_free(repo);
+}
diff --git a/src/gout.c b/src/gout.c
@@ -8,19 +8,11 @@
#include "git/file.h"
#include "git/git.h"
#include "git/reference.h"
-#include "git/repo.h"
#include "security.h"
#include "utils.h"
#include "writer/repo_writer.h"
-struct GoutOptions {
- const char* repodir;
- long long log_commit_limit; /* -1 indicates not used */
- const char* cachefile_path;
- const char* baseurl; /* base URL to make absolute RSS/Atom URI */
- RepoWriterType writer_type;
-};
-
+static void repo_callback(Git* git, void* user_data);
static void commit_callback(const GitCommit* commit, void* user_data);
static void reference_callback(const GitReference* ref, void* user_data);
static void file_callback(const GitFile* file, void* user_data);
@@ -105,7 +97,7 @@ void gout_options_free(GoutOptions* options) {
free(options);
}
-void gout_init(const GoutOptions* options) {
+void gout_init(const GoutOptions* options, Git* git) {
const char* readonly_paths[] = {options->repodir};
size_t readonly_paths_count = 1;
const char* readwrite_paths[2] = {".", NULL};
@@ -119,12 +111,17 @@ void gout_init(const GoutOptions* options) {
restrict_system_operations(
options->cachefile_path != NULL ? kGoutWithCachefile : kGout);
- gout_git_initialize();
+ git->initialize(git);
+}
+
+void gout_run(const GoutOptions* options, Git* git) {
+ git->for_repo(git, options->repodir, repo_callback, (void*)options);
}
-void gout_run(const GoutOptions* options) {
- GitRepo* repo = gitrepo_create(options->repodir);
- RepoWriter* writer = repowriter_create(options->writer_type, repo);
+static void repo_callback(Git* git, void* user_data) {
+ const GoutOptions* options = (const GoutOptions*)user_data;
+ RepoWriter* writer =
+ repowriter_create(options->writer_type, git->repo, git->fs);
if (options->log_commit_limit >= 0) {
repowriter_set_log_commit_limit(writer, options->log_commit_limit);
}
@@ -136,30 +133,25 @@ void gout_run(const GoutOptions* options) {
}
repowriter_begin(writer);
- gitrepo_for_each_commit(repo, commit_callback, writer);
- gitrepo_for_each_reference(repo, reference_callback, writer);
- gitrepo_for_each_file(repo, file_callback, writer);
+ git->for_each_commit(git, commit_callback, writer);
+ git->for_each_reference(git, reference_callback, writer);
+ git->for_each_file(git, file_callback, writer);
repowriter_end(writer);
repowriter_free(writer);
- gitrepo_free(repo);
-}
-
-void gout_shutdown(void) {
- gout_git_shutdown();
}
-void commit_callback(const GitCommit* ci, void* user_data) {
+static void commit_callback(const GitCommit* ci, void* user_data) {
RepoWriter* writer = (RepoWriter*)user_data;
repowriter_add_commit(writer, ci);
}
-void reference_callback(const GitReference* ref, void* user_data) {
+static void reference_callback(const GitReference* ref, void* user_data) {
RepoWriter* writer = (RepoWriter*)user_data;
repowriter_add_reference(writer, ref);
}
-void file_callback(const GitFile* file, void* user_data) {
+static void file_callback(const GitFile* file, void* user_data) {
RepoWriter* writer = (RepoWriter*)user_data;
repowriter_add_file(writer, file);
}
diff --git a/src/gout.h b/src/gout.h
@@ -1,12 +1,25 @@
#ifndef GOUT_GOUT_H_
#define GOUT_GOUT_H_
-typedef struct GoutOptions GoutOptions;
+#include "git/git.h"
+
+typedef enum {
+ kRepoWriterTypeHtml,
+ kRepoWriterTypeGopher,
+} RepoWriterType;
+
+typedef struct GoutOptions {
+ const char* repodir;
+ long long log_commit_limit; /* -1 indicates not used */
+ const char* cachefile_path;
+ const char* baseurl; /* base URL to make absolute RSS/Atom URI */
+ RepoWriterType writer_type;
+} GoutOptions;
+
GoutOptions* gout_options_create(int argc, const char* argv[]);
void gout_options_free(GoutOptions* options);
-void gout_init(const GoutOptions* options);
-void gout_run(const GoutOptions* options);
-void gout_shutdown(void);
+void gout_init(const GoutOptions* options, Git* git);
+void gout_run(const GoutOptions* options, Git* git);
#endif // GOUT_GOUT_H_
diff --git a/src/gout_index.c b/src/gout_index.c
@@ -5,18 +5,12 @@
#include <stdlib.h>
#include "git/git.h"
-#include "git/repo.h"
#include "security.h"
#include "third_party/openbsd/reallocarray.h"
#include "utils.h"
#include "writer/index_writer.h"
-struct GoutIndexOptions {
- const char** repo_dirs;
- size_t repo_dir_count;
- const char* me_url;
- IndexWriterType writer_type;
-};
+static void repo_callback(Git* git, void* user_data);
GoutIndexOptions* gout_index_options_create(int argc, const char* argv[]) {
GoutIndexOptions options = {
@@ -61,7 +55,7 @@ void gout_index_options_free(GoutIndexOptions* options) {
free(options);
}
-void gout_index_init(const GoutIndexOptions* options) {
+void gout_index_init(const GoutIndexOptions* options, Git* git) {
const char** readonly_paths = options->repo_dirs;
size_t readonly_paths_count = options->repo_dir_count;
const char* readwrite_paths[1] = {NULL};
@@ -70,24 +64,23 @@ void gout_index_init(const GoutIndexOptions* options) {
readwrite_paths, readwrite_paths_count);
restrict_system_operations(kGoutIndex);
- gout_git_initialize();
+ git->initialize(git);
}
-void gout_index_run(const GoutIndexOptions* options) {
- IndexWriter* writer = indexwriter_create(options->writer_type);
+void gout_index_run(const GoutIndexOptions* options, Git* git) {
+ IndexWriter* writer = indexwriter_create(options->writer_type, stdout);
if (options->me_url) {
indexwriter_set_me_url(writer, options->me_url);
}
indexwriter_begin(writer);
for (size_t i = 0; i < options->repo_dir_count; i++) {
- GitRepo* repo = gitrepo_create(options->repo_dirs[i]);
- indexwriter_add_repo(writer, repo);
- gitrepo_free(repo);
+ git->for_repo(git, options->repo_dirs[i], repo_callback, writer);
}
indexwriter_end(writer);
indexwriter_free(writer);
}
-void gout_index_shutdown(void) {
- gout_git_shutdown();
+static void repo_callback(Git* git, void* user_data) {
+ IndexWriter* writer = (IndexWriter*)user_data;
+ indexwriter_add_repo(writer, git->repo);
}
diff --git a/src/gout_index.h b/src/gout_index.h
@@ -1,12 +1,24 @@
#ifndef GOUT_GOUT_INDEX_H_
#define GOUT_GOUT_INDEX_H_
-typedef struct GoutIndexOptions GoutIndexOptions;
+#include "git/git.h"
+
+typedef enum {
+ kIndexWriterTypeHtml,
+ kIndexWriterTypeGopher,
+} IndexWriterType;
+
+typedef struct GoutIndexOptions {
+ const char** repo_dirs;
+ size_t repo_dir_count;
+ const char* me_url;
+ IndexWriterType writer_type;
+} GoutIndexOptions;
+
GoutIndexOptions* gout_index_options_create(int argc, const char* argv[]);
void gout_index_options_free(GoutIndexOptions* options);
-void gout_index_init(const GoutIndexOptions* options);
-void gout_index_run(const GoutIndexOptions* options);
-void gout_index_shutdown(void);
+void gout_index_init(const GoutIndexOptions* options, Git* git);
+void gout_index_run(const GoutIndexOptions* options, Git* git);
#endif // GOUT_GOUT_INDEX_H_
diff --git a/src/gout_index_main.c b/src/gout_index_main.c
@@ -3,6 +3,8 @@
#include <stdio.h>
#include <stdlib.h>
+#include "fs_posix.h"
+
static void gout_index_usage(const char* program_name) {
fprintf(stderr, "usage: %s [repodir...]\n", program_name);
}
@@ -13,9 +15,14 @@ int main(int argc, const char* argv[]) {
gout_index_usage(argv[0]);
exit(1);
}
- gout_index_init(options);
- gout_index_run(options);
- gout_index_shutdown();
+
+ Git* git = gout_git_create(g_fs_posix);
+ gout_index_init(options, git);
+ gout_index_run(options, git);
+
+ git->shutdown(git);
+ gout_git_free(git);
+
gout_index_options_free(options);
return 0;
}
diff --git a/src/gout_index_options_tests.c b/src/gout_index_options_tests.c
@@ -0,0 +1,58 @@
+#include "gout_index.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "utest.h"
+
+UTEST(gout_index_options, Basic) {
+ const char* argv[] = {"gout_index", "repo1", "repo2"};
+ GoutIndexOptions* options = gout_index_options_create(3, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_EQ((size_t)2, options->repo_dir_count);
+ EXPECT_STREQ("repo1", options->repo_dirs[0]);
+ EXPECT_STREQ("repo2", options->repo_dirs[1]);
+ EXPECT_EQ(NULL, options->me_url);
+ EXPECT_EQ((int)kIndexWriterTypeHtml, (int)options->writer_type);
+ gout_index_options_free(options);
+}
+
+UTEST(gout_index_options, MeUrl) {
+ const char* argv[] = {"gout_index", "-m", "https://me.com", "repo1"};
+ GoutIndexOptions* options = gout_index_options_create(4, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_STREQ("https://me.com", options->me_url);
+ EXPECT_EQ((size_t)1, options->repo_dir_count);
+ EXPECT_STREQ("repo1", options->repo_dirs[0]);
+ gout_index_options_free(options);
+}
+
+UTEST(gout_index_options, GopherWriter) {
+ const char* argv[] = {"gout_index", "-G", "repo1"};
+ GoutIndexOptions* options = gout_index_options_create(3, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_EQ((int)kIndexWriterTypeGopher, (int)options->writer_type);
+ gout_index_options_free(options);
+}
+
+UTEST(gout_index_options, HtmlWriter) {
+ const char* argv[] = {"gout_index", "-H", "repo1"};
+ GoutIndexOptions* options = gout_index_options_create(3, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_EQ((int)kIndexWriterTypeHtml, (int)options->writer_type);
+ gout_index_options_free(options);
+}
+
+UTEST(gout_index_options, Empty) {
+ const char* argv[] = {"gout_index"};
+ GoutIndexOptions* options = gout_index_options_create(1, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_EQ((size_t)0, options->repo_dir_count);
+ gout_index_options_free(options);
+}
+
+UTEST(gout_index_options, MissingMeUrl) {
+ const char* argv[] = {"gout_index", "-m"};
+ GoutIndexOptions* options = gout_index_options_create(2, argv);
+ EXPECT_EQ(NULL, options);
+}
diff --git a/src/gout_main.c b/src/gout_main.c
@@ -3,6 +3,8 @@
#include <stdio.h>
#include <stdlib.h>
+#include "fs_posix.h"
+
static void gout_usage(const char* program_name) {
fprintf(stderr,
"usage: %s [-c cachefile | -l commits] [-u baseurl] repodir\n",
@@ -15,9 +17,14 @@ int main(int argc, const char* argv[]) {
gout_usage(argv[0]);
exit(1);
}
- gout_init(options);
- gout_run(options);
- gout_shutdown();
+
+ Git* git = gout_git_create(g_fs_posix);
+ gout_init(options, git);
+ gout_run(options, git);
+
+ git->shutdown(git);
+ gout_git_free(git);
+
gout_options_free(options);
return 0;
}
diff --git a/src/gout_options_tests.c b/src/gout_options_tests.c
@@ -0,0 +1,88 @@
+#include "gout.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "utest.h"
+
+UTEST(gout_options, Basic) {
+ const char* argv[] = {"gout", "repo"};
+ GoutOptions* options = gout_options_create(2, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_STREQ("repo", options->repodir);
+ EXPECT_EQ((long long)-1, options->log_commit_limit);
+ EXPECT_EQ(NULL, options->cachefile_path);
+ EXPECT_STREQ("", options->baseurl);
+ EXPECT_EQ((int)kRepoWriterTypeHtml, (int)options->writer_type);
+ gout_options_free(options);
+}
+
+UTEST(gout_options, LogLimit) {
+ const char* argv[] = {"gout", "-l", "50", "repo"};
+ GoutOptions* options = gout_options_create(4, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_STREQ("repo", options->repodir);
+ EXPECT_EQ((long long)50, options->log_commit_limit);
+ gout_options_free(options);
+}
+
+UTEST(gout_options, Cachefile) {
+ const char* argv[] = {"gout", "-c", "cache.db", "repo"};
+ GoutOptions* options = gout_options_create(4, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_STREQ("repo", options->repodir);
+ EXPECT_STREQ("cache.db", options->cachefile_path);
+ gout_options_free(options);
+}
+
+UTEST(gout_options, BaseUrl) {
+ const char* argv[] = {"gout", "-u", "https://example.com", "repo"};
+ GoutOptions* options = gout_options_create(4, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_STREQ("https://example.com", options->baseurl);
+ gout_options_free(options);
+}
+
+UTEST(gout_options, GopherWriter) {
+ const char* argv[] = {"gout", "-G", "repo"};
+ GoutOptions* options = gout_options_create(3, argv);
+ ASSERT_NE(NULL, options);
+ EXPECT_EQ((int)kRepoWriterTypeGopher, (int)options->writer_type);
+ gout_options_free(options);
+}
+
+UTEST(gout_options, MutuallyExclusiveLogCache) {
+ const char* argv[] = {"gout", "-l", "50", "-c", "cache.db", "repo"};
+ GoutOptions* options = gout_options_create(6, argv);
+ EXPECT_EQ(NULL, options);
+}
+
+UTEST(gout_options, MutuallyExclusiveCacheLog) {
+ const char* argv[] = {"gout", "-c", "cache.db", "-l", "50", "repo"};
+ GoutOptions* options = gout_options_create(6, argv);
+ EXPECT_EQ(NULL, options);
+}
+
+UTEST(gout_options, MissingRepo) {
+ const char* argv[] = {"gout", "-l", "50"};
+ GoutOptions* options = gout_options_create(3, argv);
+ EXPECT_EQ(NULL, options);
+}
+
+UTEST(gout_options, DuplicateRepo) {
+ const char* argv[] = {"gout", "repo1", "repo2"};
+ GoutOptions* options = gout_options_create(3, argv);
+ EXPECT_EQ(NULL, options);
+}
+
+UTEST(gout_options, InvalidLogLimit) {
+ const char* argv[] = {"gout", "-l", "not-a-number", "repo"};
+ GoutOptions* options = gout_options_create(4, argv);
+ EXPECT_EQ(NULL, options);
+}
+
+UTEST(gout_options, NegativeLogLimit) {
+ const char* argv[] = {"gout", "-l", "-10", "repo"};
+ GoutOptions* options = gout_options_create(4, argv);
+ EXPECT_EQ(NULL, options);
+}
diff --git a/src/test_utils.h b/src/test_utils.h
@@ -0,0 +1,32 @@
+#ifndef GOUT_TEST_UTILS_H_
+#define GOUT_TEST_UTILS_H_
+
+#include <string.h>
+#include "utest.h"
+
+#define EXPECT_STR_SEQUENCE(buf, ...) \
+ do { \
+ const char* _seq[] = {__VA_ARGS__}; \
+ const char* _p = (buf); \
+ for (size_t _i = 0; _i < sizeof(_seq) / sizeof(_seq[0]); _i++) { \
+ _p = strstr(_p, _seq[_i]); \
+ if (_p == NULL) { \
+ EXPECT_NE(NULL, _p); \
+ break; \
+ } \
+ } \
+ } while (0)
+
+#define EXPECT_STR_COUNT(buf, str, expected) \
+ do { \
+ int _count = 0; \
+ const char* _p = (buf); \
+ size_t _len = strlen(str); \
+ while ((_p = strstr(_p, str)) != NULL) { \
+ _count++; \
+ _p += _len; \
+ } \
+ EXPECT_EQ((int)(expected), _count); \
+ } while (0)
+
+#endif // GOUT_TEST_UTILS_H_
diff --git a/src/utils.c b/src/utils.c
@@ -7,16 +7,20 @@
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
+#include <unistd.h>
-#include "third_party/openbsd/strlcat.h"
#include "third_party/openbsd/strlcpy.h"
char* path_concat(const char* p1, const char* p2) {
size_t p1_len = p1 ? strlen(p1) : 0;
size_t p2_len = p2 ? strlen(p2) : 0;
- if (p1_len == 0) return estrdup(p2 ? p2 : "");
- if (p2_len == 0) return estrdup(p1);
+ if (p1_len == 0) {
+ return estrdup(p2 ? p2 : "");
+ }
+ if (p2_len == 0) {
+ return estrdup(p1);
+ }
bool p1_slash = p1[p1_len - 1] == '/';
bool p2_slash = p2[0] == '/';
@@ -45,29 +49,6 @@ char* path_concat(const char* p1, const char* p2) {
return out;
}
-int mkdirp(const char* path) {
- char* mut_path = estrdup(path);
-
- for (char* p = mut_path + (mut_path[0] == '/'); *p; p++) {
- if (*p != '/') {
- continue;
- }
- *p = '\0';
- if (mkdir(mut_path, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) {
- free(mut_path);
- return -1;
- }
- *p = '/';
- }
- if (mkdir(mut_path, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) {
- free(mut_path);
- return -1;
- }
-
- free(mut_path);
- return 0;
-}
-
void* ecalloc(size_t count, size_t size) {
void* ptr = calloc(count, size);
if (!ptr) {
@@ -92,14 +73,6 @@ size_t estrlcpy(char* dst, const char* src, size_t dsize) {
return len;
}
-FILE* efopen(const char* filename, const char* flags) {
- FILE* fp = fopen(filename, flags);
- if (!fp) {
- err(1, "fopen: '%s'", filename);
- }
- return fp;
-}
-
bool is_safe_repo_path(const char* path) {
if (path[0] == '/') {
return false;
diff --git a/src/utils.h b/src/utils.h
@@ -4,13 +4,25 @@
#include <stdbool.h>
#include <stdio.h>
+#include <sys/stat.h>
+
+/* File system operations. */
+typedef struct FileSystem {
+ int (*mkdir)(const char* path, mode_t mode);
+ int (*mkdirp)(const char* path);
+ FILE* (*fopen)(const char* path, const char* mode);
+ int (*fclose)(FILE* stream);
+ int (*mkstemp)(char* template);
+ int (*rename)(const char* oldpath, const char* newpath);
+ int (*chmod)(const char* path, mode_t mode);
+ int (*access)(const char* path, int amode);
+ char* (*realpath)(const char* path, char* resolved_path);
+} FileSystem;
+
/* Concatenates path parts to "p1/p2". Returns a dynamically allocated string.
* Exits on failure. */
char* path_concat(const char* p1, const char* p2);
-/* Recursively creates the directories specified by path, like mkdir -p. */
-int mkdirp(const char* path);
-
/* Behaves as calloc but exits on failure. */
void* ecalloc(size_t count, size_t size);
@@ -20,9 +32,6 @@ char* estrdup(const char* s);
/* Behaves as strlcpy but exits on failure or truncation. */
size_t estrlcpy(char* dst, const char* src, size_t dsize);
-/* Opens the specified file. Terminates with error on failure. */
-FILE* efopen(const char* filename, const char* flags);
-
/* Validates that a path is safe to use. Returns true if safe. */
bool is_safe_repo_path(const char* path);
diff --git a/src/writer/atom/atom.c b/src/writer/atom/atom.c
@@ -14,18 +14,16 @@ struct Atom {
size_t remaining_commits;
};
-Atom* atom_create(const GitRepo* repo, AtomType type) {
+Atom* atom_create(const GitRepo* repo, FILE* out) {
Atom* atom = ecalloc(1, sizeof(Atom));
atom->repo = repo;
atom->baseurl = "";
- const char* filename = (type == kAtomTypeAll) ? "atom.xml" : "tags.xml";
- atom->out = efopen(filename, "w");
+ atom->out = out;
atom->remaining_commits = 100;
return atom;
}
void atom_free(Atom* atom) {
- fclose(atom->out);
atom->out = NULL;
free(atom);
}
@@ -38,10 +36,14 @@ void atom_begin(Atom* atom) {
fprintf(atom->out, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
fprintf(atom->out, "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n");
fprintf(atom->out, "<title>");
- print_xml_encoded(atom->out, gitrepo_short_name(atom->repo));
+ if (atom->repo->short_name) {
+ print_xml_encoded(atom->out, atom->repo->short_name);
+ }
fprintf(atom->out, ", branch HEAD</title>\n");
fprintf(atom->out, "<subtitle>");
- print_xml_encoded(atom->out, gitrepo_description(atom->repo));
+ if (atom->repo->description) {
+ print_xml_encoded(atom->out, atom->repo->description);
+ }
fprintf(atom->out, "</subtitle>\n");
}
@@ -57,7 +59,9 @@ void atom_add_commit(Atom* atom,
atom->remaining_commits--;
fprintf(out, "<entry>\n");
- fprintf(out, "<id>%s</id>\n", commit->oid);
+ if (commit->oid) {
+ fprintf(out, "<id>%s</id>\n", commit->oid);
+ }
fprintf(out, "<published>");
print_time_z(out, commit->author_time);
@@ -70,9 +74,9 @@ void atom_add_commit(Atom* atom,
if (commit->summary) {
fprintf(out, "<title>");
if (tag && tag[0] != '\0') {
- fputc('[', out);
+ fprintf(out, "[");
print_xml_encoded(out, tag);
- fputc(']', out);
+ fprintf(out, "]");
}
print_xml_encoded(out, commit->summary);
fprintf(out, "</title>\n");
@@ -84,28 +88,38 @@ void atom_add_commit(Atom* atom,
fprintf(out, "href=\"%s%s\" />\n", atom->baseurl, path);
fprintf(out, "<author>\n<name>");
- print_xml_encoded(out, commit->author_name);
+ if (commit->author_name) {
+ print_xml_encoded(out, commit->author_name);
+ }
fprintf(out, "</name>\n<email>");
- print_xml_encoded(out, commit->author_email);
+ if (commit->author_email) {
+ print_xml_encoded(out, commit->author_email);
+ }
fprintf(out, "</email>\n</author>\n");
fprintf(out, "<content>");
- fprintf(out, "commit %s\n", commit->oid);
+ if (commit->oid) {
+ fprintf(out, "commit %s\n", commit->oid);
+ }
const char* parentoid = commit->parentoid;
- if (parentoid[0]) {
+ if (parentoid && parentoid[0]) {
fprintf(out, "parent %s\n", parentoid);
}
fprintf(out, "Author: ");
- print_xml_encoded(out, commit->author_name);
+ if (commit->author_name) {
+ print_xml_encoded(out, commit->author_name);
+ }
fprintf(out, " <");
- print_xml_encoded(out, commit->author_email);
+ if (commit->author_email) {
+ print_xml_encoded(out, commit->author_email);
+ }
fprintf(out, ">\n");
fprintf(out, "Date: ");
print_time(out, commit->author_time, commit->author_timezone_offset);
fprintf(out, "\n");
const char* message = commit->message;
if (message) {
- fputc('\n', out);
+ fprintf(out, "\n");
print_xml_encoded(out, message);
}
fprintf(out, "\n</content>\n</entry>\n");
diff --git a/src/writer/atom/atom.h b/src/writer/atom/atom.h
@@ -1,22 +1,16 @@
#ifndef GOUT_WRITER_ATOM_ATOM_H_
#define GOUT_WRITER_ATOM_ATOM_H_
+#include <stdio.h>
+
#include "git/commit.h"
#include "git/repo.h"
/* Atom RSS output file. */
typedef struct Atom Atom;
-/* Atom RSS feed type. */
-typedef enum {
- /* Atom RSS feed containing all commits. */
- kAtomTypeAll,
- /* Atom RSS feed containing only tags. */
- kAtomTypeTags,
-} AtomType;
-
-/* Allocate a new Atom RSS output file. */
-Atom* atom_create(const GitRepo* repo, AtomType type);
+/* Allocate a new Atom RSS output file writing to the specified stream. */
+Atom* atom_create(const GitRepo* repo, FILE* out);
/* Frees the specified Atom RSS output file. */
void atom_free(Atom* atom);
diff --git a/src/writer/atom/atom_tests.c b/src/writer/atom/atom_tests.c
@@ -0,0 +1,169 @@
+#include "writer/atom/atom.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "git/commit.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+UTEST(atom, begin) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+ ASSERT_NE(NULL, out);
+
+ GitRepo repo = {
+ .short_name = "test-repo",
+ .description = "A test repository",
+ };
+
+ Atom* atom = atom_create(&repo, out);
+ atom_begin(atom);
+ atom_free(atom);
+ fflush(out);
+ fclose(out);
+
+ EXPECT_STR_SEQUENCE(buf, //
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", //
+ "<feed xmlns=\"http://www.w3.org/2005/Atom\">", //
+ "<title>test-repo, branch HEAD</title>", //
+ "<subtitle>A test repository</subtitle>");
+
+ free(buf);
+}
+
+UTEST(atom, add_commit) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+ ASSERT_NE(NULL, out);
+
+ GitRepo repo = {.short_name = "test-repo"};
+ Atom* atom = atom_create(&repo, out);
+ atom_set_baseurl(atom, "https://example.com/");
+
+ GitCommit commit = {
+ .oid = "abc1234567890",
+ .parentoid = "def0987654321",
+ .summary = "Fix a bug",
+ .message = "Detailed description of the fix.",
+ .author_name = "Test User",
+ .author_email = "test@example.com",
+ .author_time = 1702031400, /* 2023-12-08 10:30:00 UTC */
+ .author_timezone_offset = 540, /* +09:00 */
+ .commit_time = 1702035000, /* 2023-12-08 11:30:00 UTC */
+ .commit_timezone_offset = 540,
+ };
+
+ atom_add_commit(atom, &commit, "commit/abc.html", "text/html", "v1.0");
+ atom_free(atom);
+ fflush(out);
+ fclose(out);
+
+ EXPECT_STR_SEQUENCE(buf, //
+ "<entry>", //
+ "<id>abc1234567890</id>", //
+ "<published>2023-12-08T10:30:00Z</published>", //
+ "<updated>2023-12-08T11:30:00Z</updated>", //
+ "<title>[v1.0]Fix a bug</title>", //
+ "<link rel=\"alternate\" type=\"text/html\" "
+ "href=\"https://example.com/commit/abc.html\" />", //
+ "<author>", //
+ "<name>Test User</name>", //
+ "<email>test@example.com</email>", //
+ "</author>", //
+ "<content>", //
+ "commit abc1234567890", //
+ "parent def0987654321", //
+ "Author: Test User <test@example.com>", //
+ "Detailed description of the fix.", //
+ "</content>", //
+ "</entry>");
+
+ free(buf);
+}
+
+UTEST(atom, multiple_commits) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+ ASSERT_NE(NULL, out);
+
+ GitRepo repo = {.short_name = "test-repo"};
+ Atom* atom = atom_create(&repo, out);
+
+ GitCommit commit1 = {
+ .oid = "sha1",
+ .summary = "Commit 1",
+ .author_name = "User 1",
+ .author_email = "user1@mail.com",
+ };
+ GitCommit commit2 = {
+ .oid = "sha2",
+ .summary = "Commit 2",
+ .author_name = "User 2",
+ .author_email = "user2@mail.com",
+ };
+
+ atom_add_commit(atom, &commit1, "c1", "text/html", "");
+ atom_add_commit(atom, &commit2, "c2", "text/html", "");
+ atom_free(atom);
+ fflush(out);
+ fclose(out);
+
+ EXPECT_STR_SEQUENCE(buf, //
+ "<entry>", //
+ "<id>sha1</id>", //
+ "<title>Commit 1</title>", //
+ "</entry>", //
+ "<entry>", //
+ "<id>sha2</id>", //
+ "<title>Commit 2</title>", //
+ "</entry>");
+
+ free(buf);
+}
+
+UTEST(atom, end) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+ ASSERT_NE(NULL, out);
+
+ GitRepo repo = {.short_name = "test-repo"};
+ Atom* atom = atom_create(&repo, out);
+ atom_end(atom);
+ atom_free(atom);
+ fflush(out);
+ fclose(out);
+
+ EXPECT_STREQ("</feed>\n", buf);
+
+ free(buf);
+}
+
+UTEST(atom, limit_commits) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+ ASSERT_NE(NULL, out);
+
+ GitRepo repo = {.short_name = "test-repo"};
+ Atom* atom = atom_create(&repo, out);
+
+ GitCommit commit = {
+ .oid = "sha", .author_name = "user", .author_email = "mail"};
+ for (int i = 0; i < 105; i++) {
+ atom_add_commit(atom, &commit, "path", "text/html", "");
+ }
+
+ fflush(out);
+ EXPECT_STR_COUNT(buf, "<entry>", 100);
+
+ atom_free(atom);
+ fclose(out);
+ free(buf);
+}
diff --git a/src/writer/cache/cache.c b/src/writer/cache/cache.c
@@ -12,8 +12,6 @@
struct Cache {
bool can_add_commits;
- char* cache_path;
- char* temp_cache_path;
WriteCommitRow write_commit_row;
FILE* cache_in;
FILE* cache_out;
@@ -21,20 +19,17 @@ struct Cache {
bool wrote_lastoid_out;
};
-static const char* kTempCachePath = "cache.XXXXXXXXXXXX";
-static const mode_t kReadWriteAll =
- S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
-
-Cache* cache_create(const char* cache_path, WriteCommitRow write_func) {
+Cache* cache_create(FILE* cache_in,
+ FILE* cache_out,
+ WriteCommitRow write_func) {
Cache* cache = ecalloc(1, sizeof(Cache));
cache->can_add_commits = true;
- cache->cache_path = estrdup(cache_path);
- cache->temp_cache_path = estrdup(kTempCachePath);
cache->write_commit_row = write_func;
+ cache->cache_in = cache_in;
+ cache->cache_out = cache_out;
- // Open previous cache for reading, if it exists, and read lastoid.
+ // Read lastoid from previous cache, if it exists.
cache->lastoid_in = ecalloc(kOidLen, sizeof(char));
- cache->cache_in = fopen(cache_path, "r");
if (cache->cache_in) {
// OID + '\n' + '\0'.
char buf[kOidLen + 1];
@@ -48,20 +43,11 @@ Cache* cache_create(const char* cache_path, WriteCommitRow write_func) {
cache->lastoid_in[--len] = '\0';
}
} else {
- warnx("corrupt cachefile: %s", cache_path);
+ warnx("corrupt cachefile");
}
}
cache->wrote_lastoid_out = false;
- // Create temporary cache and open for writing.
- int out_fd = mkstemp(cache->temp_cache_path);
- if (out_fd == -1) {
- err(1, "mkstemp");
- }
- cache->cache_out = fdopen(out_fd, "w");
- if (!cache->cache_out) {
- err(1, "fdopen");
- }
return cache;
}
@@ -69,19 +55,6 @@ void cache_free(Cache* cache) {
if (!cache) {
return;
}
- free(cache->cache_path);
- cache->cache_path = NULL;
- free(cache->temp_cache_path);
- cache->temp_cache_path = NULL;
- // Clean up in case the cache wasn't written.
- if (cache->cache_in) {
- fclose(cache->cache_in);
- cache->cache_in = NULL;
- }
- if (cache->cache_out) {
- fclose(cache->cache_out);
- cache->cache_out = NULL;
- }
free(cache->lastoid_in);
cache->lastoid_in = NULL;
free(cache);
@@ -107,7 +80,7 @@ void cache_add_commit_row(Cache* cache, const GitCommit* commit) {
cache->write_commit_row(cache->cache_out, commit);
}
-void cache_write(Cache* cache) {
+void cache_finish(Cache* cache) {
if (cache->cache_in) {
// If we didn't write any records, copy the previous cache lastoid.
if (!cache->wrote_lastoid_out) {
@@ -125,28 +98,10 @@ void cache_write(Cache* cache) {
break;
}
}
- fclose(cache->cache_in);
- cache->cache_in = NULL;
- }
- fclose(cache->cache_out);
- cache->cache_out = NULL;
-
- // Replace previous cache with new cache.
- if (rename(cache->temp_cache_path, cache->cache_path)) {
- err(1, "rename");
- }
-
- // Set the cache to read-write for user, group, other, modulo umask.
- mode_t mask;
- umask(mask = umask(0));
- if (chmod(cache->cache_path, kReadWriteAll & ~mask)) {
- err(1, "chmod");
}
}
-void cache_copy_log(Cache* cache, FILE* out) {
- FILE* fcache = efopen(cache->cache_path, "r");
-
+void cache_copy_log(FILE* fcache, FILE* out) {
// Copy the log lines to out.
char* line = NULL;
size_t len = 0;
@@ -160,6 +115,4 @@ void cache_copy_log(Cache* cache, FILE* out) {
fprintf(out, "%s", line);
}
free(line);
-
- fclose(fcache);
}
diff --git a/src/writer/cache/cache.h b/src/writer/cache/cache.h
@@ -37,11 +37,12 @@ typedef void (*WriteCommitRow)(FILE* out, const GitCommit* commit);
/* Allocates a cache.
*
- * If an existing file exists at cache_path, it is opened for reading and the
- * OID header is read.
+ * The cache_in stream is used to read the previously cached OID and log data.
+ * It may be NULL if no previous cache exists.
*
- * A temporary file will be opened for writing which will be used to overwrite
- * the file at cache_path (if any), when cache_write is invoked.
+ * The cache_out stream is used to write the updated cache.
+ *
+ * Ownership of the streams is NOT taken by the Cache object.
*
* The WriteCommitRow parameter is a callback that is passed the output
* stream of the cache and a GitCommit struct, and writes formatted output to
@@ -49,7 +50,7 @@ typedef void (*WriteCommitRow)(FILE* out, const GitCommit* commit);
* is used to generating the commit log itself since the contents of this cache
* will be inserted into the commit log for all commits after the cached OID.
*/
-Cache* cache_create(const char* cache_path, WriteCommitRow write_func);
+Cache* cache_create(FILE* cache_in, FILE* cache_out, WriteCommitRow write_func);
/* Frees the specified cache. */
void cache_free(Cache* cache);
@@ -69,11 +70,14 @@ bool cache_can_add_commits(const Cache* cache);
*/
void cache_add_commit_row(Cache* cache, const GitCommit* commit);
-/* Writes the cache to cache_path, overwriting any existing file.
+/* Finishes the cache processing by appending any remaining data from the
+ * input stream to the output stream.
*/
-void cache_write(Cache* cache);
+void cache_finish(Cache* cache);
-/* Copies the contents of the cache to the specified output stream. */
-void cache_copy_log(Cache* cache, FILE* out);
+/* Copies the contents of the cache stream to the specified output stream,
+ * skipping the OID header.
+ */
+void cache_copy_log(FILE* cache_in, FILE* out);
#endif // GOUT_WRITER_CACHE_CACHE_H_
diff --git a/src/writer/cache/cache_tests.c b/src/writer/cache/cache_tests.c
@@ -0,0 +1,111 @@
+#include "writer/cache/cache.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "git/commit.h"
+#include "utest.h"
+
+static void mock_write_commit(FILE* out, const GitCommit* commit) {
+ fprintf(out, "row:%s\n", commit->oid);
+}
+
+UTEST(cache, new_cache) {
+ char* out_buf = NULL;
+ size_t out_size = 0;
+ FILE* out = open_memstream(&out_buf, &out_size);
+
+ Cache* cache = cache_create(NULL, out, mock_write_commit);
+ GitCommit c1 = {.oid = "sha1"};
+ GitCommit c2 = {.oid = "sha2"};
+
+ EXPECT_TRUE(cache_can_add_commits(cache));
+ cache_add_commit_row(cache, &c1);
+ EXPECT_TRUE(cache_can_add_commits(cache));
+ cache_add_commit_row(cache, &c2);
+ EXPECT_TRUE(cache_can_add_commits(cache));
+
+ cache_finish(cache);
+ cache_free(cache);
+ fflush(out);
+ fclose(out);
+
+ const char* expected = "sha1\nrow:sha1\nrow:sha2\n";
+ EXPECT_STREQ(expected, out_buf);
+
+ free(out_buf);
+}
+
+UTEST(cache, incremental_update) {
+ const char* prev_cache = "sha2\nrow:sha2\nrow:sha3\n";
+ FILE* in = fmemopen((void*)prev_cache, strlen(prev_cache), "r");
+
+ char* out_buf = NULL;
+ size_t out_size = 0;
+ FILE* out = open_memstream(&out_buf, &out_size);
+
+ Cache* cache = cache_create(in, out, mock_write_commit);
+
+ GitCommit c1 = {.oid = "sha1"};
+ GitCommit c2 = {.oid = "sha2"};
+ GitCommit c3 = {.oid = "sha3"};
+
+ EXPECT_TRUE(cache_can_add_commits(cache));
+ cache_add_commit_row(cache, &c1);
+
+ EXPECT_TRUE(cache_can_add_commits(cache));
+ cache_add_commit_row(cache, &c2);
+ EXPECT_FALSE(cache_can_add_commits(cache));
+
+ cache_add_commit_row(cache, &c3);
+
+ cache_finish(cache);
+ cache_free(cache);
+ fclose(in);
+ fflush(out);
+ fclose(out);
+
+ const char* expected = "sha1\nrow:sha1\nrow:sha2\nrow:sha3\n";
+ EXPECT_STREQ(expected, out_buf);
+
+ free(out_buf);
+}
+
+UTEST(cache, corrupt_input) {
+ const char* prev_cache = "";
+ FILE* in = fmemopen((void*)prev_cache, 0, "r");
+
+ char* out_buf = NULL;
+ size_t out_size = 0;
+ FILE* out = open_memstream(&out_buf, &out_size);
+
+ Cache* cache = cache_create(in, out, mock_write_commit);
+ GitCommit c1 = {.oid = "sha1"};
+ cache_add_commit_row(cache, &c1);
+ cache_finish(cache);
+ cache_free(cache);
+ fclose(in);
+ fflush(out);
+ fclose(out);
+
+ EXPECT_STREQ("sha1\nrow:sha1\n", out_buf);
+ free(out_buf);
+}
+
+UTEST(cache, copy_log) {
+ const char* cache_data = "lastsha\nline1\nline2\n";
+ FILE* in = fmemopen((void*)cache_data, strlen(cache_data), "r");
+
+ char* out_buf = NULL;
+ size_t out_size = 0;
+ FILE* out = open_memstream(&out_buf, &out_size);
+
+ cache_copy_log(in, out);
+ fflush(out);
+ fclose(in);
+ fclose(out);
+
+ EXPECT_STREQ("line1\nline2\n", out_buf);
+ free(out_buf);
+}
diff --git a/src/writer/gopher/commit.c b/src/writer/gopher/commit.c
@@ -15,6 +15,7 @@
struct GopherCommit {
FILE* out;
+ const FileSystem* fs;
GopherPage* page;
};
@@ -32,18 +33,23 @@ static void gopher_commit_write_diff_hunk(GopherCommit* commit,
const GitHunk* hunk);
GopherCommit* gopher_commit_create(const GitRepo* repo,
+ const FileSystem* fs,
const char* oid,
const char* title) {
GopherCommit* commit = ecalloc(1, sizeof(GopherCommit));
+ commit->fs = fs;
char filename[PATH_MAX];
int r = snprintf(filename, sizeof(filename), "%s.gph", oid);
if (r < 0 || (size_t)r >= sizeof(filename)) {
errx(1, "snprintf: filename truncated or error");
}
char* path = path_concat("commit", filename);
- commit->out = efopen(path, "w");
+ commit->out = fs->fopen(path, "w");
+ if (!commit->out) {
+ err(1, "fopen: %s", path);
+ }
free(path);
- commit->page = gopher_page_create(commit->out, repo, title, "");
+ commit->page = gopher_page_create(commit->out, repo, fs, title, "");
return commit;
}
@@ -51,7 +57,7 @@ void gopher_commit_free(GopherCommit* commit) {
if (!commit) {
return;
}
- fclose(commit->out);
+ commit->fs->fclose(commit->out);
commit->out = NULL;
gopher_page_free(commit->page);
commit->page = NULL;
@@ -92,8 +98,8 @@ void gopher_commit_end(GopherCommit* commit) {
gopher_page_end(commit->page);
}
-void gopher_commit_write_summary(GopherCommit* commit,
- const GitCommit* git_commit) {
+static void gopher_commit_write_summary(GopherCommit* commit,
+ const GitCommit* git_commit) {
FILE* out = commit->out;
const char* oid = git_commit->oid;
fprintf(out, "[1|commit %s|../commit/%s.gph|server|port]\n", oid, oid);
@@ -123,8 +129,8 @@ void gopher_commit_write_summary(GopherCommit* commit,
}
}
-void gopher_commit_write_diffstat(GopherCommit* commit,
- const GitCommit* git_commit) {
+static void gopher_commit_write_diffstat(GopherCommit* commit,
+ const GitCommit* git_commit) {
fprintf(commit->out, "Diffstat:\n");
size_t delta_count = git_commit->deltas_len;
for (size_t i = 0; i < delta_count; i++) {
@@ -132,8 +138,8 @@ void gopher_commit_write_diffstat(GopherCommit* commit,
}
}
-void gopher_commit_write_diffstat_row(GopherCommit* commit,
- const GitDelta* delta) {
+static void gopher_commit_write_diffstat_row(GopherCommit* commit,
+ const GitDelta* delta) {
static const size_t kGraphWidth = 30;
FILE* out = commit->out;
@@ -161,8 +167,8 @@ void gopher_commit_write_diffstat_row(GopherCommit* commit,
fprintf(out, "\n");
}
-void gopher_commit_write_diff_content(GopherCommit* commit,
- const GitCommit* git_commit) {
+static void gopher_commit_write_diff_content(GopherCommit* commit,
+ const GitCommit* git_commit) {
FILE* out = commit->out;
size_t addcount = git_commit->addcount;
size_t delcount = git_commit->delcount;
@@ -179,8 +185,8 @@ void gopher_commit_write_diff_content(GopherCommit* commit,
}
}
-void gopher_commit_write_diff_delta(GopherCommit* commit,
- const GitDelta* delta) {
+static void gopher_commit_write_diff_delta(GopherCommit* commit,
+ const GitDelta* delta) {
FILE* out = commit->out;
fprintf(out, "[1|diff --git a/");
print_gopher_link(out, delta->old_file_path);
@@ -200,7 +206,8 @@ void gopher_commit_write_diff_delta(GopherCommit* commit,
}
}
-void gopher_commit_write_diff_hunk(GopherCommit* commit, const GitHunk* hunk) {
+static void gopher_commit_write_diff_hunk(GopherCommit* commit,
+ const GitHunk* hunk) {
FILE* out = commit->out;
// Output header. e.g. @@ -0,0 +1,3 @@
diff --git a/src/writer/gopher/commit.h b/src/writer/gopher/commit.h
@@ -3,10 +3,12 @@
#include "git/commit.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct GopherCommit GopherCommit;
GopherCommit* gopher_commit_create(const GitRepo* repo,
+ const FileSystem* fs,
const char* oid,
const char* title);
void gopher_commit_free(GopherCommit* commit);
diff --git a/src/writer/gopher/commit_tests.c b/src/writer/gopher/commit_tests.c
@@ -0,0 +1,125 @@
+#include "writer/gopher/commit.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/commit.h"
+#include "git/delta.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct gopher_commit {
+ int dummy;
+};
+
+UTEST_F_SETUP(gopher_commit) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(gopher_commit) {}
+
+UTEST_F(gopher_commit, basic) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherCommit* commit_writer =
+ gopher_commit_create(&repo, g_fs_inmemory, "sha123", "Commit Title");
+ ASSERT_NE(NULL, commit_writer);
+
+ GitCommit commit = {
+ .oid = "sha123",
+ .parentoid = "parent456",
+ .summary = "Fix a bug",
+ .message = "Detailed description.",
+ .author_name = "Author Name",
+ .author_email = "author@example.com",
+ .author_time = 1702031400,
+ .author_timezone_offset = 0,
+ };
+
+ gopher_commit_begin(commit_writer);
+ gopher_commit_add_commit(commit_writer, &commit);
+ gopher_commit_end(commit_writer);
+ gopher_commit_free(commit_writer);
+
+ const char* buf = inmemory_fs_get_buffer("commit/sha123.gph");
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Header/Page Title */
+ EXPECT_STR_SEQUENCE(buf, "Commit Title");
+
+ /* Verify Author Info */
+ EXPECT_STR_SEQUENCE(buf, "Author: Author Name", "author@example.com");
+
+ /* Verify Parent */
+ EXPECT_STR_SEQUENCE(buf, "parent", "parent456");
+
+ /* Verify Message */
+ EXPECT_STR_SEQUENCE(buf, "Detailed description.");
+}
+
+UTEST_F(gopher_commit, diff) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherCommit* commit_writer =
+ gopher_commit_create(&repo, g_fs_inmemory, "sha123", "Commit Title");
+
+ GitHunkLine l1 = {.id = 1,
+ .old_lineno = 1,
+ .new_lineno = 1,
+ .content = " unchanged",
+ .content_len = 10};
+ GitHunkLine l2 = {.id = 2,
+ .old_lineno = -1,
+ .new_lineno = 2,
+ .content = "added line",
+ .content_len = 10};
+ GitHunkLine l3 = {.id = 3,
+ .old_lineno = 2,
+ .new_lineno = -1,
+ .content = "removed line",
+ .content_len = 12};
+
+ GitHunkLine* lines[] = {&l1, &l2, &l3};
+ GitHunk hunk = {
+ .id = 1, .header = "@@ -1,2 +1,2 @@", .lines = lines, .lines_len = 3};
+ GitHunk* hunks[] = {&hunk};
+
+ GitDelta delta = {
+ .status = 'M',
+ .old_file_path = "file.txt",
+ .new_file_path = "file.txt",
+ .addcount = 1,
+ .delcount = 1,
+ .hunks = hunks,
+ .hunks_len = 1,
+ };
+ GitDelta* deltas[] = {&delta};
+
+ GitCommit commit = {
+ .oid = "sha123",
+ .parentoid = "",
+ .author_name = "User",
+ .author_email = "user@mail.com",
+ .deltas = deltas,
+ .deltas_len = 1,
+ .filecount = 1,
+ .addcount = 1,
+ .delcount = 1,
+ };
+
+ gopher_commit_begin(commit_writer);
+ gopher_commit_add_commit(commit_writer, &commit);
+ gopher_commit_end(commit_writer);
+ gopher_commit_free(commit_writer);
+
+ const char* buf = inmemory_fs_get_buffer("commit/sha123.gph");
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Diffstat */
+ EXPECT_STR_SEQUENCE(buf, "Diffstat:", "file.txt", "1");
+
+ /* Verify Diff Content */
+ EXPECT_STR_SEQUENCE(buf, "[1|diff --git", "file.txt", "@@ -1,2 +1,2 @@",
+ " unchanged", "+added line", "-removed line");
+}
diff --git a/src/writer/gopher/fileblob.c b/src/writer/gopher/fileblob.c
@@ -14,16 +14,20 @@
struct GopherFileBlob {
const GitRepo* repo;
+ const FileSystem* fs;
FILE* out;
GopherPage* page;
};
-GopherFileBlob* gopher_fileblob_create(const GitRepo* repo, const char* path) {
+GopherFileBlob* gopher_fileblob_create(const GitRepo* repo,
+ const FileSystem* fs,
+ const char* path) {
if (!is_safe_repo_path(path)) {
errx(1, "unsafe path: %s", path);
}
GopherFileBlob* blob = ecalloc(1, sizeof(GopherFileBlob));
blob->repo = repo;
+ blob->fs = fs;
// Create directories.
char filename_buffer[PATH_MAX];
@@ -38,8 +42,11 @@ GopherFileBlob* gopher_fileblob_create(const GitRepo* repo, const char* path) {
if (!d) {
err(1, "dirname");
}
- mkdirp(d);
- blob->out = efopen(out_path, "w");
+ fs->mkdirp(d);
+ blob->out = fs->fopen(out_path, "w");
+ if (!blob->out) {
+ err(1, "fopen: %s", out_path);
+ }
// Compute the relative path.
char* relpath = relpath_from_dir(d);
@@ -50,7 +57,7 @@ GopherFileBlob* gopher_fileblob_create(const GitRepo* repo, const char* path) {
if (!title) {
err(1, "basename");
}
- blob->page = gopher_page_create(blob->out, repo, title, relpath);
+ blob->page = gopher_page_create(blob->out, repo, fs, title, relpath);
free(out_path);
free(path_copy);
@@ -63,7 +70,7 @@ void gopher_fileblob_free(GopherFileBlob* blob) {
if (!blob) {
return;
}
- fclose(blob->out);
+ blob->fs->fclose(blob->out);
blob->out = NULL;
gopher_page_free(blob->page);
blob->page = NULL;
diff --git a/src/writer/gopher/fileblob.h b/src/writer/gopher/fileblob.h
@@ -3,10 +3,13 @@
#include "git/file.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct GopherFileBlob GopherFileBlob;
-GopherFileBlob* gopher_fileblob_create(const GitRepo* repo, const char* path);
+GopherFileBlob* gopher_fileblob_create(const GitRepo* repo,
+ const FileSystem* fs,
+ const char* path);
void gopher_fileblob_free(GopherFileBlob* blob);
void gopher_fileblob_begin(GopherFileBlob* blob);
void gopher_fileblob_add_file(GopherFileBlob* blob, const GitFile* file);
diff --git a/src/writer/gopher/fileblob_tests.c b/src/writer/gopher/fileblob_tests.c
@@ -0,0 +1,93 @@
+#include "writer/gopher/fileblob.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/file.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct gopher_fileblob {
+ int dummy;
+};
+
+UTEST_F_SETUP(gopher_fileblob) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(gopher_fileblob) {}
+
+UTEST_F(gopher_fileblob, basic) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherFileBlob* blob_writer =
+ gopher_fileblob_create(&repo, g_fs_inmemory, "src/main.c");
+ ASSERT_NE(NULL, blob_writer);
+
+ const char* content = "int main() {\n return 0;\n}\n";
+ GitFile file = {
+ .repo_path = "src/main.c",
+ .content = (char*)content,
+ .size_bytes = strlen(content),
+ .size_lines = 3,
+ };
+
+ gopher_fileblob_begin(blob_writer);
+ gopher_fileblob_add_file(blob_writer, &file);
+ gopher_fileblob_end(blob_writer);
+ gopher_fileblob_free(blob_writer);
+
+ const char* buf = inmemory_fs_get_buffer("file/src/main.c.gph");
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Header/Filename */
+ EXPECT_STR_SEQUENCE(buf, "main.c", "(27B)", "---");
+
+ /* Verify Content and Line Numbers (6 spaces for line num) */
+ EXPECT_STR_SEQUENCE(buf, " 1 int main()", " 2 return 0;",
+ " 3 }");
+}
+
+UTEST_F(gopher_fileblob, binary) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherFileBlob* blob_writer =
+ gopher_fileblob_create(&repo, g_fs_inmemory, "logo.png");
+
+ GitFile file = {
+ .repo_path = "logo.png",
+ .size_bytes = 100,
+ .size_lines = -1, /* Binary */
+ };
+
+ gopher_fileblob_begin(blob_writer);
+ gopher_fileblob_add_file(blob_writer, &file);
+ gopher_fileblob_end(blob_writer);
+ gopher_fileblob_free(blob_writer);
+
+ const char* buf = inmemory_fs_get_buffer("file/logo.png.gph");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "logo.png", "Binary file.");
+}
+
+UTEST_F(gopher_fileblob, too_large) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherFileBlob* blob_writer =
+ gopher_fileblob_create(&repo, g_fs_inmemory, "big.txt");
+
+ GitFile file = {
+ .repo_path = "big.txt",
+ .size_bytes = 1000000,
+ .size_lines = -2, /* Too large */
+ };
+
+ gopher_fileblob_begin(blob_writer);
+ gopher_fileblob_add_file(blob_writer, &file);
+ gopher_fileblob_end(blob_writer);
+ gopher_fileblob_free(blob_writer);
+
+ const char* buf = inmemory_fs_get_buffer("file/big.txt.gph");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "big.txt", "File too large to display.");
+}
diff --git a/src/writer/gopher/files.c b/src/writer/gopher/files.c
@@ -1,5 +1,6 @@
#include "writer/gopher/files.h"
+#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
@@ -10,15 +11,20 @@
struct GopherFiles {
const GitRepo* repo;
+ const FileSystem* fs;
FILE* out;
GopherPage* page;
};
-GopherFiles* gopher_files_create(const GitRepo* repo) {
+GopherFiles* gopher_files_create(const GitRepo* repo, const FileSystem* fs) {
GopherFiles* files = ecalloc(1, sizeof(GopherFiles));
files->repo = repo;
- files->out = efopen("files.gph", "w");
- files->page = gopher_page_create(files->out, repo, "Files", "");
+ files->fs = fs;
+ files->out = fs->fopen("files.gph", "w");
+ if (!files->out) {
+ err(1, "fopen: files.gph");
+ }
+ files->page = gopher_page_create(files->out, repo, fs, "Files", "");
return files;
}
@@ -26,7 +32,7 @@ void gopher_files_free(GopherFiles* files) {
if (!files) {
return;
}
- fclose(files->out);
+ files->fs->fclose(files->out);
files->out = NULL;
gopher_page_free(files->page);
files->page = NULL;
diff --git a/src/writer/gopher/files.h b/src/writer/gopher/files.h
@@ -3,10 +3,11 @@
#include "git/file.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct GopherFiles GopherFiles;
-GopherFiles* gopher_files_create(const GitRepo* repo);
+GopherFiles* gopher_files_create(const GitRepo* repo, const FileSystem* fs);
void gopher_files_free(GopherFiles* files);
void gopher_files_begin(GopherFiles* files);
void gopher_files_add_file(GopherFiles* files, const GitFile* file);
diff --git a/src/writer/gopher/files_tests.c b/src/writer/gopher/files_tests.c
@@ -0,0 +1,71 @@
+#include "writer/gopher/files.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/file.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct gopher_files {
+ int dummy;
+};
+
+UTEST_F_SETUP(gopher_files) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(gopher_files) {}
+
+UTEST_F(gopher_files, basic) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherFiles* files = gopher_files_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, files);
+
+ GitFile f1 = {
+ .type = kFileTypeFile,
+ .mode = "-rw-r--r--",
+ .display_path = "README.md",
+ .repo_path = "README.md",
+ .size_bytes = 100,
+ .size_lines = 5,
+ };
+
+ gopher_files_begin(files);
+ gopher_files_add_file(files, &f1);
+ gopher_files_end(files);
+ gopher_files_free(files);
+
+ const char* buf = inmemory_fs_get_buffer("files.gph");
+ ASSERT_NE(NULL, buf);
+ /* Header + Entry */
+ EXPECT_STR_SEQUENCE(buf, "Mode", "Name", "Size", "-rw-r--r--", "README.md",
+ "5L", "|file/README.md.gph");
+}
+
+UTEST_F(gopher_files, binary_file) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherFiles* files = gopher_files_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, files);
+
+ GitFile bin = {
+ .type = kFileTypeFile,
+ .mode = "-rwxr-xr-x",
+ .display_path = "logo.png",
+ .repo_path = "logo.png",
+ .size_bytes = 1024,
+ .size_lines = -1,
+ };
+
+ gopher_files_begin(files);
+ gopher_files_add_file(files, &bin);
+ gopher_files_end(files);
+ gopher_files_free(files);
+
+ const char* buf = inmemory_fs_get_buffer("files.gph");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "-rwxr-xr-x", "logo.png", "1024B");
+}
diff --git a/src/writer/gopher/index_writer.c b/src/writer/gopher/index_writer.c
@@ -11,9 +11,8 @@ struct GopherIndexWriter {
GopherRepoIndex* index;
};
-GopherIndexWriter* gopher_indexwriter_create(void) {
+GopherIndexWriter* gopher_indexwriter_create(FILE* out) {
GopherIndexWriter* writer = ecalloc(1, sizeof(GopherIndexWriter));
- FILE* out = stdout;
writer->index = gopher_repoindex_create(out);
return writer;
}
@@ -31,7 +30,8 @@ void gopher_indexwriter_begin(GopherIndexWriter* writer) {
gopher_repoindex_begin(writer->index);
}
-void gopher_indexwriter_add_repo(GopherIndexWriter* writer, GitRepo* repo) {
+void gopher_indexwriter_add_repo(GopherIndexWriter* writer,
+ const GitRepo* repo) {
gopher_repoindex_add_repo(writer->index, repo);
}
diff --git a/src/writer/gopher/index_writer.h b/src/writer/gopher/index_writer.h
@@ -3,12 +3,15 @@
#include "git/repo.h"
+#include <stdio.h>
+
typedef struct GopherIndexWriter GopherIndexWriter;
-GopherIndexWriter* gopher_indexwriter_create(void);
+GopherIndexWriter* gopher_indexwriter_create(FILE* out);
void gopher_indexwriter_free(GopherIndexWriter* writer);
void gopher_indexwriter_begin(GopherIndexWriter* writer);
-void gopher_indexwriter_add_repo(GopherIndexWriter* writer, GitRepo* repo);
+void gopher_indexwriter_add_repo(GopherIndexWriter* writer,
+ const GitRepo* repo);
void gopher_indexwriter_end(GopherIndexWriter* writer);
#endif // GOUT_WRITER_GOPHER_INDEX_WRITER_H_
diff --git a/src/writer/gopher/log.c b/src/writer/gopher/log.c
@@ -1,5 +1,6 @@
#include "writer/gopher/log.h"
+#include <err.h>
#include <stdint.h>
#include <stdlib.h>
@@ -8,10 +9,18 @@
#include "writer/cache/cache.h"
#include "writer/gopher/page.h"
+#include <sys/stat.h>
+#include <unistd.h>
+
struct GopherLog {
const GitRepo* repo;
+ const FileSystem* fs;
FILE* out;
Cache* cache;
+ char* cache_path;
+ char* temp_cache_path;
+ FILE* cache_in;
+ FILE* cache_out;
GopherPage* page;
size_t remaining_commits;
size_t unlogged_commits;
@@ -19,11 +28,19 @@ struct GopherLog {
static void write_commit_row(FILE* out, const GitCommit* commit);
-GopherLog* gopher_log_create(const GitRepo* repo) {
+static const char* kTempCachePath = "cache.XXXXXXXXXXXX";
+static const mode_t kReadWriteAll =
+ S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
+
+GopherLog* gopher_log_create(const GitRepo* repo, const FileSystem* fs) {
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->fs = fs;
+ log->out = fs->fopen("log.gph", "w");
+ if (!log->out) {
+ err(1, "fopen: log.gph");
+ }
+ log->page = gopher_page_create(log->out, repo, fs, "Log", "");
log->remaining_commits = SIZE_MAX;
log->unlogged_commits = 0;
return log;
@@ -33,17 +50,41 @@ void gopher_log_free(GopherLog* log) {
if (!log) {
return;
}
- fclose(log->out);
+ log->fs->fclose(log->out);
log->out = NULL;
cache_free(log->cache);
log->cache = NULL;
+ free(log->cache_path);
+ free(log->temp_cache_path);
+ if (log->cache_in) {
+ log->fs->fclose(log->cache_in);
+ }
+ if (log->cache_out) {
+ /* TODO: When fdopen is added to FileSystem, update to fs->fclose. */
+ fclose(log->cache_out);
+ }
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);
+ log->cache_path = estrdup(cachefile);
+ log->temp_cache_path = estrdup(kTempCachePath);
+
+ log->cache_in = log->fs->fopen(cachefile, "r");
+ int out_fd = log->fs->mkstemp(log->temp_cache_path);
+ if (out_fd == -1) {
+ err(1, "mkstemp: %s", log->temp_cache_path);
+ }
+ /* TODO: Consider adding fdopen to the FileSystem VTable. */
+ log->cache_out = fdopen(out_fd, "w");
+ if (!log->cache_out) {
+ close(out_fd);
+ err(1, "fdopen: %s", log->temp_cache_path);
+ }
+
+ log->cache = cache_create(log->cache_in, log->cache_out, write_commit_row);
}
void gopher_log_set_commit_limit(GopherLog* log, size_t count) {
@@ -76,8 +117,31 @@ void gopher_log_add_commit(GopherLog* log, const GitCommit* commit) {
void gopher_log_end(GopherLog* log) {
FILE* out = log->out;
if (log->cache) {
- cache_write(log->cache);
- cache_copy_log(log->cache, log->out);
+ cache_finish(log->cache);
+ if (log->cache_in) {
+ log->fs->fclose(log->cache_in);
+ log->cache_in = NULL;
+ }
+ /* TODO: Use fs->fclose() for consistency. */
+ fclose(log->cache_out);
+ log->cache_out = NULL;
+
+ if (log->fs->rename(log->temp_cache_path, log->cache_path)) {
+ err(1, "rename: %s -> %s", log->temp_cache_path, log->cache_path);
+ }
+
+ mode_t mask;
+ umask(mask = umask(0));
+ if (log->fs->chmod(log->cache_path, kReadWriteAll & ~mask)) {
+ err(1, "chmod: %s", log->cache_path);
+ }
+
+ FILE* fcache = log->fs->fopen(log->cache_path, "r");
+ if (!fcache) {
+ err(1, "fopen: %s", log->cache_path);
+ }
+ cache_copy_log(fcache, log->out);
+ log->fs->fclose(fcache);
} else if (log->unlogged_commits > 0) {
size_t count = log->unlogged_commits;
fprintf(out, "%16.16s %zu more commits remaining, fetch the repository\n",
@@ -88,7 +152,7 @@ void gopher_log_end(GopherLog* log) {
gopher_page_end(log->page);
}
-void write_commit_row(FILE* out, const GitCommit* commit) {
+static void write_commit_row(FILE* out, const GitCommit* commit) {
fprintf(out, "[1|");
print_time_short(out, commit->author_time);
fprintf(out, " ");
diff --git a/src/writer/gopher/log.h b/src/writer/gopher/log.h
@@ -6,10 +6,11 @@
#include "git/commit.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct GopherLog GopherLog;
-GopherLog* gopher_log_create(const GitRepo* repo);
+GopherLog* gopher_log_create(const GitRepo* repo, const FileSystem* fs);
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);
diff --git a/src/writer/gopher/log_tests.c b/src/writer/gopher/log_tests.c
@@ -0,0 +1,112 @@
+#include "writer/gopher/log.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/commit.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct gopher_log {
+ int dummy;
+};
+
+UTEST_F_SETUP(gopher_log) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(gopher_log) {}
+
+UTEST_F(gopher_log, begin) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherLog* log = gopher_log_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, log);
+ gopher_log_begin(log);
+ gopher_log_free(log);
+
+ const char* buf = inmemory_fs_get_buffer("log.gph");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "test-repo", "Date", "Commit message", "Author");
+}
+
+UTEST_F(gopher_log, add_commit) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherLog* log = gopher_log_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, log);
+
+ GitCommit commit = {
+ .oid = "abc1234",
+ .summary = "Fix bug",
+ .author_name = "User",
+ .author_time = 1702031400,
+ };
+
+ gopher_log_begin(log);
+ gopher_log_add_commit(log, &commit);
+ gopher_log_end(log);
+ gopher_log_free(log);
+
+ const char* buf = inmemory_fs_get_buffer("log.gph");
+ ASSERT_NE(NULL, buf);
+ /* Gopher format: [1|2023-12-08 10:30 Fix bug
+ * User|commit/abc1234.gph|server|port] */
+ EXPECT_STR_SEQUENCE(buf, "[1|", "2023-12-08 10:30", "Fix bug", "User",
+ "|commit/abc1234.gph");
+}
+
+UTEST_F(gopher_log, commit_limit) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherLog* log = gopher_log_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, log);
+ gopher_log_set_commit_limit(log, 2);
+
+ GitCommit commit = {.oid = "sha", .author_name = "user", .summary = "msg"};
+
+ gopher_log_begin(log);
+ gopher_log_add_commit(log, &commit);
+ gopher_log_add_commit(log, &commit);
+ gopher_log_add_commit(log, &commit);
+ gopher_log_add_commit(log, &commit);
+
+ gopher_log_end(log);
+ gopher_log_free(log);
+
+ const char* buf = inmemory_fs_get_buffer("log.gph");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_COUNT(buf, "msg", 2);
+ EXPECT_NE(NULL, strstr(buf, "2 more commits remaining"));
+}
+
+UTEST_F(gopher_log, cache_integration) {
+ const char* old_cache_content = "sha_old\n[1|Old Commit|path|server|port]\n";
+ FILE* pre = g_fs_inmemory->fopen("cache.db", "w");
+ fprintf(pre, "%s", old_cache_content);
+ g_fs_inmemory->fclose(pre);
+
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherLog* log = gopher_log_create(&repo, g_fs_inmemory);
+ gopher_log_set_cachefile(log, "cache.db");
+
+ GitCommit c_new = {
+ .oid = "sha_new", .summary = "New Commit", .author_name = "User"};
+ GitCommit c_old = {
+ .oid = "sha_old", .summary = "Old Commit", .author_name = "User"};
+
+ gopher_log_begin(log);
+ gopher_log_add_commit(log, &c_new);
+ gopher_log_add_commit(log, &c_old);
+ gopher_log_end(log);
+ gopher_log_free(log);
+
+ const char* buf = inmemory_fs_get_buffer("log.gph");
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_STR_SEQUENCE(buf, "New Commit", "Old Commit");
+
+ const char* cache_buf = inmemory_fs_get_buffer("cache.db");
+ ASSERT_NE(NULL, cache_buf);
+ EXPECT_TRUE(strncmp(cache_buf, "sha_new", 7) == 0);
+}
diff --git a/src/writer/gopher/page.c b/src/writer/gopher/page.c
@@ -9,17 +9,20 @@
struct GopherPage {
FILE* out;
const GitRepo* repo;
+ const FileSystem* fs;
char* title;
char* relpath;
};
GopherPage* gopher_page_create(FILE* out,
const GitRepo* repo,
+ const FileSystem* fs,
const char* title,
const char* relpath) {
GopherPage* page = ecalloc(1, sizeof(GopherPage));
page->out = out;
page->repo = repo;
+ page->fs = fs;
page->title = estrdup(title);
page->relpath = estrdup(relpath);
return page;
@@ -39,18 +42,18 @@ void gopher_page_free(GopherPage* page) {
void gopher_page_begin(GopherPage* page) {
FILE* out = page->out;
print_gopher_text(out, page->title, false);
- const char* short_name = gitrepo_short_name(page->repo);
+ const char* short_name = page->repo->short_name;
if (page->title[0] != '\0' && short_name && short_name[0] != '\0') {
fprintf(out, " - ");
print_gopher_text(out, short_name, false);
}
- const char* description = gitrepo_description(page->repo);
+ const char* description = page->repo->description;
if (page->title[0] != '\0' && description && description[0] != '\0') {
fprintf(out, " - ");
print_gopher_text(out, description, false);
fprintf(out, "\n");
}
- const char* clone_url = gitrepo_clone_url(page->repo);
+ const char* clone_url = page->repo->clone_url;
if (clone_url && clone_url[0] != '\0') {
fprintf(out, "[h|git clone ");
print_gopher_link(out, clone_url);
@@ -62,17 +65,17 @@ void gopher_page_begin(GopherPage* page) {
fprintf(out, "[1|Files|%sfiles.gph|server|port]\n", page->relpath);
fprintf(out, "[1|Refs|%srefs.gph|server|port]\n", page->relpath);
- const char* submodules = gitrepo_submodules(page->repo);
+ const char* submodules = page->repo->submodules;
if (submodules && submodules[0] != '\0') {
fprintf(out, "[1|Submodules|%sfile/%s.gph|server|port]\n", page->relpath,
submodules);
}
- const char* readme = gitrepo_readme(page->repo);
+ const char* readme = page->repo->readme;
if (readme && readme[0] != '\0') {
fprintf(out, "[1|README|%sfile/%s.gph|server|port]\n", page->relpath,
readme);
}
- const char* license = gitrepo_license(page->repo);
+ const char* license = page->repo->license;
if (license && license[0] != '\0') {
fprintf(out, "[1|LICENSE|%sfile/%s.gph|server|port]\n", page->relpath,
license);
diff --git a/src/writer/gopher/page.h b/src/writer/gopher/page.h
@@ -4,11 +4,13 @@
#include <stdio.h>
#include "git/repo.h"
+#include "utils.h"
typedef struct GopherPage GopherPage;
GopherPage* gopher_page_create(FILE* out,
const GitRepo* repo,
+ const FileSystem* fs,
const char* title,
const char* relpath);
void gopher_page_free(GopherPage* page);
diff --git a/src/writer/gopher/page_tests.c b/src/writer/gopher/page_tests.c
@@ -0,0 +1,53 @@
+#include "writer/gopher/page.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct gopher_page {
+ int dummy;
+};
+
+UTEST_F_SETUP(gopher_page) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(gopher_page) {}
+
+UTEST_F(gopher_page, basic) {
+ GitRepo repo = {
+ .short_name = "test-repo",
+ .description = "Repo description",
+ .clone_url = "git://example.com/repo.git",
+ .license = "LICENSE",
+ };
+
+ FILE* out = g_fs_inmemory->fopen("test.gph", "w");
+ GopherPage* page =
+ gopher_page_create(out, &repo, g_fs_inmemory, "Page Title", "rel");
+ ASSERT_NE(NULL, page);
+
+ gopher_page_begin(page);
+ gopher_page_end(page);
+ g_fs_inmemory->fclose(out);
+ gopher_page_free(page);
+
+ const char* buf = inmemory_fs_get_buffer("test.gph");
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Header/Title */
+ EXPECT_STR_SEQUENCE(buf, "Page Title", "test-repo", "Repo description");
+
+ /* Verify Navigation */
+ EXPECT_STR_SEQUENCE(buf, "[1|Log|", "log.gph", "[1|Files|", "files.gph",
+ "[1|Refs|", "refs.gph");
+
+ /* Verify Metadata */
+ EXPECT_STR_SEQUENCE(buf, "git clone", "git://example.com/repo.git",
+ "LICENSE");
+}
diff --git a/src/writer/gopher/refs.c b/src/writer/gopher/refs.c
@@ -1,5 +1,6 @@
#include "writer/gopher/refs.h"
+#include <err.h>
#include <stdio.h>
#include <stdlib.h>
@@ -16,6 +17,7 @@ typedef struct {
struct GopherRefs {
const GitRepo* repo;
+ const FileSystem* fs;
FILE* out;
GopherPage* page;
GopherRefsTable* branches;
@@ -31,9 +33,9 @@ static void gopher_refstable_add_ref(GopherRefsTable* table,
const GitReference* ref);
static void gopher_refstable_end(GopherRefsTable* table);
-GopherRefsTable* gopher_refstable_create(const char* title,
- const char* id,
- FILE* out) {
+static 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);
@@ -41,7 +43,7 @@ GopherRefsTable* gopher_refstable_create(const char* title,
return table;
}
-void gopher_refstable_free(GopherRefsTable* table) {
+static void gopher_refstable_free(GopherRefsTable* table) {
if (!table) {
return;
}
@@ -52,7 +54,7 @@ void gopher_refstable_free(GopherRefsTable* table) {
free(table);
}
-void gopher_refstable_begin(GopherRefsTable* table) {
+static void gopher_refstable_begin(GopherRefsTable* table) {
FILE* out = table->out;
fprintf(out, "%s\n", table->title);
fprintf(out, " %-32.32s", "Name");
@@ -60,7 +62,8 @@ void gopher_refstable_begin(GopherRefsTable* table) {
fprintf(out, " %s\n", "Author");
}
-void gopher_refstable_add_ref(GopherRefsTable* table, const GitReference* ref) {
+static void gopher_refstable_add_ref(GopherRefsTable* table,
+ const GitReference* ref) {
GitCommit* commit = ref->commit;
FILE* out = table->out;
@@ -73,16 +76,20 @@ void gopher_refstable_add_ref(GopherRefsTable* table, const GitReference* ref) {
fprintf(out, "\n");
}
-void gopher_refstable_end(GopherRefsTable* table) {
+static void gopher_refstable_end(GopherRefsTable* table) {
FILE* out = table->out;
fprintf(out, "\n");
}
-GopherRefs* gopher_refs_create(const GitRepo* repo) {
+GopherRefs* gopher_refs_create(const GitRepo* repo, const FileSystem* fs) {
GopherRefs* refs = ecalloc(1, sizeof(GopherRefs));
refs->repo = repo;
- refs->out = efopen("refs.gph", "w");
- refs->page = gopher_page_create(refs->out, repo, "Refs", "");
+ refs->fs = fs;
+ refs->out = fs->fopen("refs.gph", "w");
+ if (!refs->out) {
+ err(1, "fopen: refs.gph");
+ }
+ refs->page = gopher_page_create(refs->out, repo, fs, "Refs", "");
return refs;
}
@@ -90,7 +97,7 @@ void gopher_refs_free(GopherRefs* refs) {
if (!refs) {
return;
}
- fclose(refs->out);
+ refs->fs->fclose(refs->out);
refs->out = NULL;
gopher_page_free(refs->page);
refs->page = NULL;
diff --git a/src/writer/gopher/refs.h b/src/writer/gopher/refs.h
@@ -3,10 +3,11 @@
#include "git/reference.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct GopherRefs GopherRefs;
-GopherRefs* gopher_refs_create(const GitRepo* repo);
+GopherRefs* gopher_refs_create(const GitRepo* repo, const FileSystem* fs);
void gopher_refs_free(GopherRefs* refs);
void gopher_refs_begin(GopherRefs* refs);
void gopher_refs_add_ref(GopherRefs* refs, const GitReference* ref);
diff --git a/src/writer/gopher/refs_tests.c b/src/writer/gopher/refs_tests.c
@@ -0,0 +1,98 @@
+#include "writer/gopher/refs.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/commit.h"
+#include "git/reference.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct gopher_refs {
+ int dummy;
+};
+
+UTEST_F_SETUP(gopher_refs) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(gopher_refs) {}
+
+UTEST_F(gopher_refs, branches) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherRefs* refs = gopher_refs_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, refs);
+
+ GitCommit commit = {
+ .author_name = "User A",
+ .author_time = 1702031400,
+ };
+ GitReference ref = {
+ .type = kReftypeBranch,
+ .shorthand = "main",
+ .commit = &commit,
+ };
+
+ gopher_refs_begin(refs);
+ gopher_refs_add_ref(refs, &ref);
+ gopher_refs_end(refs);
+ gopher_refs_free(refs);
+
+ const char* buf = inmemory_fs_get_buffer("refs.gph");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "Branches", "Name", "Last commit date", "Author",
+ "main", "2023-12-08 10:30", "User A");
+}
+
+UTEST_F(gopher_refs, tags) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherRefs* refs = gopher_refs_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, refs);
+
+ GitCommit commit = {
+ .author_name = "User B",
+ .author_time = 1702035000,
+ };
+ GitReference ref = {
+ .type = kReftypeTag,
+ .shorthand = "v1.0",
+ .commit = &commit,
+ };
+
+ gopher_refs_begin(refs);
+ gopher_refs_add_ref(refs, &ref);
+ gopher_refs_end(refs);
+ gopher_refs_free(refs);
+
+ const char* buf = inmemory_fs_get_buffer("refs.gph");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "Tags", "Name", "Last commit date", "Author", "v1.0",
+ "2023-12-08 11:30", "User B");
+}
+
+UTEST_F(gopher_refs, both) {
+ GitRepo repo = {.short_name = "test-repo"};
+ GopherRefs* refs = gopher_refs_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, refs);
+
+ GitCommit commit1 = {.author_name = "User 1", .author_time = 1000};
+ GitReference branch = {
+ .type = kReftypeBranch, .shorthand = "main", .commit = &commit1};
+
+ GitCommit commit2 = {.author_name = "User 2", .author_time = 2000};
+ GitReference tag = {
+ .type = kReftypeTag, .shorthand = "v1", .commit = &commit2};
+
+ gopher_refs_begin(refs);
+ gopher_refs_add_ref(refs, &branch);
+ gopher_refs_add_ref(refs, &tag);
+ gopher_refs_end(refs);
+ gopher_refs_free(refs);
+
+ const char* buf = inmemory_fs_get_buffer("refs.gph");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "Branches", "main", "Tags", "v1");
+}
diff --git a/src/writer/gopher/repo_index.c b/src/writer/gopher/repo_index.c
@@ -3,7 +3,6 @@
#include <stdlib.h>
#include "format.h"
-#include "git/commit.h"
#include "utils.h"
struct GopherRepoIndex {
@@ -20,7 +19,6 @@ void gopher_repoindex_free(GopherRepoIndex* index) {
if (!index) {
return;
}
- fclose(index->out);
index->out = NULL;
free(index);
}
@@ -33,20 +31,17 @@ void gopher_repoindex_begin(GopherRepoIndex* index) {
fprintf(out, "%s\n", "Last commit");
}
-static void print_author_time(const GitCommit* commit, void* user_data) {
- FILE* out = (FILE*)user_data;
- print_time_short(out, commit->author_time);
-}
-
-void gopher_repoindex_add_repo(GopherRepoIndex* index, GitRepo* repo) {
+void gopher_repoindex_add_repo(GopherRepoIndex* index, const GitRepo* repo) {
FILE* out = index->out;
fprintf(out, "[1|");
- print_gopher_link_padded(out, gitrepo_short_name(repo), 20, ' ');
+ print_gopher_link_padded(out, repo->short_name, 20, ' ');
fprintf(out, " ");
- print_gopher_link_padded(out, gitrepo_description(repo), 39, ' ');
+ print_gopher_link_padded(out, repo->description, 39, ' ');
fprintf(out, " ");
- gitrepo_for_commit(repo, "HEAD", print_author_time, out);
- fprintf(out, "|%s/log.gph|server|port]\n", gitrepo_short_name(repo));
+ if (repo->last_commit_time > 0) {
+ print_time_short(out, repo->last_commit_time);
+ }
+ fprintf(out, "|%s/log.gph|server|port]\n", repo->short_name);
}
void gopher_repoindex_end(GopherRepoIndex* index) {
diff --git a/src/writer/gopher/repo_index.h b/src/writer/gopher/repo_index.h
@@ -10,7 +10,7 @@ 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, GitRepo* repo);
+void gopher_repoindex_add_repo(GopherRepoIndex* index, const GitRepo* repo);
void gopher_repoindex_end(GopherRepoIndex* index);
#endif // GOUT_WRITER_GOPHER_REPOINDEX_H_
diff --git a/src/writer/gopher/repo_index_tests.c b/src/writer/gopher/repo_index_tests.c
@@ -0,0 +1,66 @@
+#include "writer/gopher/repo_index.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+UTEST(gopher_repo_index, basic) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+
+ GopherRepoIndex* index = gopher_repoindex_create(out);
+ ASSERT_NE(NULL, index);
+
+ GitRepo r1 = {
+ .short_name = "repo1",
+ .description = "Desc 1",
+ .owner = "Owner 1",
+ .last_commit_time = 1702031400,
+ };
+
+ gopher_repoindex_begin(index);
+ gopher_repoindex_add_repo(index, &r1);
+ gopher_repoindex_end(index);
+ gopher_repoindex_free(index);
+ fclose(out);
+
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Header */
+ EXPECT_STR_SEQUENCE(buf, "Repositories", "Name", "Description",
+ "Last commit");
+
+ /* Verify Repo Row */
+ EXPECT_STR_SEQUENCE(buf, "[1|", "repo1", "Desc 1", "2023-12-08 10:30",
+ "|repo1/log.gph");
+
+ free(buf);
+}
+
+UTEST(gopher_repo_index, multiple) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+
+ GopherRepoIndex* index = gopher_repoindex_create(out);
+
+ GitRepo r1 = {.short_name = "a", .description = "d1", .owner = "o1"};
+ GitRepo r2 = {.short_name = "b", .description = "d2", .owner = "o2"};
+
+ gopher_repoindex_begin(index);
+ gopher_repoindex_add_repo(index, &r1);
+ gopher_repoindex_add_repo(index, &r2);
+ gopher_repoindex_end(index);
+ gopher_repoindex_free(index);
+ fclose(out);
+
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "a", "d1", "b", "d2");
+
+ free(buf);
+}
diff --git a/src/writer/gopher/repo_writer.c b/src/writer/gopher/repo_writer.c
@@ -20,21 +20,34 @@
struct GopherRepoWriter {
const GitRepo* repo;
+ const FileSystem* fs;
GopherRefs* refs;
GopherLog* log;
Atom* atom;
+ FILE* atom_out;
Atom* tags;
+ FILE* tags_out;
GopherFiles* files;
};
-GopherRepoWriter* gopher_repowriter_create(const GitRepo* repo) {
+GopherRepoWriter* gopher_repowriter_create(const GitRepo* repo,
+ const FileSystem* fs) {
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);
+ writer->fs = fs;
+ writer->refs = gopher_refs_create(repo, fs);
+ writer->log = gopher_log_create(repo, fs);
+ writer->atom_out = fs->fopen("atom.xml", "w");
+ if (!writer->atom_out) {
+ err(1, "fopen: atom.xml");
+ }
+ writer->atom = atom_create(repo, writer->atom_out);
+ writer->tags_out = fs->fopen("tags.xml", "w");
+ if (!writer->tags_out) {
+ err(1, "fopen: tags.xml");
+ }
+ writer->tags = atom_create(repo, writer->tags_out);
+ writer->files = gopher_files_create(repo, fs);
return writer;
}
@@ -48,8 +61,16 @@ void gopher_repowriter_free(GopherRepoWriter* writer) {
writer->log = NULL;
atom_free(writer->atom);
writer->atom = NULL;
+ if (writer->atom_out) {
+ writer->fs->fclose(writer->atom_out);
+ writer->atom_out = NULL;
+ }
atom_free(writer->tags);
writer->tags = NULL;
+ if (writer->tags_out) {
+ writer->fs->fclose(writer->tags_out);
+ writer->tags_out = NULL;
+ }
gopher_files_free(writer->files);
writer->files = NULL;
free(writer);
@@ -72,8 +93,8 @@ void gopher_repowriter_set_baseurl(GopherRepoWriter* writer,
}
void gopher_repowriter_begin(GopherRepoWriter* writer) {
- mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
- mkdir("file", S_IRWXU | S_IRWXG | S_IRWXO);
+ writer->fs->mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
+ writer->fs->mkdir("file", S_IRWXU | S_IRWXG | S_IRWXO);
gopher_refs_begin(writer->refs);
gopher_log_begin(writer->log);
@@ -95,8 +116,8 @@ void gopher_repowriter_add_commit(GopherRepoWriter* writer,
if (gopher_log_can_add_commits(writer->log)) {
gopher_log_add_commit(writer->log, git_commit);
- GopherCommit* commit = gopher_commit_create(writer->repo, git_commit->oid,
- git_commit->summary);
+ GopherCommit* commit = gopher_commit_create(
+ writer->repo, writer->fs, git_commit->oid, git_commit->summary);
gopher_commit_begin(commit);
gopher_commit_add_commit(commit, git_commit);
gopher_commit_end(commit);
@@ -123,7 +144,8 @@ void gopher_repowriter_add_reference(GopherRepoWriter* writer,
void gopher_repowriter_add_file(GopherRepoWriter* writer, const GitFile* file) {
gopher_files_add_file(writer->files, file);
- GopherFileBlob* blob = gopher_fileblob_create(writer->repo, file->repo_path);
+ GopherFileBlob* blob =
+ gopher_fileblob_create(writer->repo, writer->fs, file->repo_path);
gopher_fileblob_begin(blob);
gopher_fileblob_add_file(blob, file);
gopher_fileblob_end(blob);
diff --git a/src/writer/gopher/repo_writer.h b/src/writer/gopher/repo_writer.h
@@ -7,10 +7,12 @@
#include "git/file.h"
#include "git/reference.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct GopherRepoWriter GopherRepoWriter;
-GopherRepoWriter* gopher_repowriter_create(const GitRepo* repo);
+GopherRepoWriter* gopher_repowriter_create(const GitRepo* repo,
+ const FileSystem* fs);
void gopher_repowriter_free(GopherRepoWriter* writer);
void gopher_repowriter_set_log_cachefile(GopherRepoWriter* writer,
const char* cachefile);
diff --git a/src/writer/gopher/repo_writer_tests.c b/src/writer/gopher/repo_writer_tests.c
@@ -0,0 +1,67 @@
+#include "writer/gopher/repo_writer.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/commit.h"
+#include "git/repo.h"
+#include "utest.h"
+
+struct gopher_repo_writer {
+ int dummy;
+};
+
+UTEST_F_SETUP(gopher_repo_writer) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(gopher_repo_writer) {}
+
+UTEST_F(gopher_repo_writer, orchestration) {
+ GitRepo repo = {
+ .name = "test-repo.git",
+ .short_name = "test-repo",
+ .description = "A test repository",
+ .owner = "Test Owner",
+ .clone_url = "git://example.com/test-repo.git",
+ .submodules = "",
+ .readme = "",
+ .license = "",
+ };
+ GopherRepoWriter* writer = gopher_repowriter_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, writer);
+
+ GitCommit commit = {
+ .oid = "sha123",
+ .parentoid = "",
+ .summary = "Test commit",
+ .message = "Detailed message.",
+ .author_name = "User",
+ .author_email = "user@example.com",
+ };
+
+ gopher_repowriter_begin(writer);
+ gopher_repowriter_add_commit(writer, &commit);
+ gopher_repowriter_end(writer);
+ gopher_repowriter_free(writer);
+
+ /* Verify orchestration by checking multiple output files. */
+
+ /* 1. Log file exists and contains the commit message. */
+ const char* log_buf = inmemory_fs_get_buffer("log.gph");
+ ASSERT_NE(NULL, log_buf);
+ EXPECT_NE(NULL, strstr(log_buf, "Test commit"));
+
+ /* 2. Atom feed exists and contains the commit entry. */
+ const char* atom_buf = inmemory_fs_get_buffer("atom.xml");
+ ASSERT_NE(NULL, atom_buf);
+ EXPECT_NE(NULL, strstr(atom_buf, "<title>Test commit</title>"));
+
+ /* 3. Individual commit page exists. */
+ const char* commit_buf = inmemory_fs_get_buffer("commit/sha123.gph");
+ ASSERT_NE(NULL, commit_buf);
+ EXPECT_NE(NULL, strstr(commit_buf, "Test commit"));
+ EXPECT_NE(NULL, strstr(commit_buf, "sha123"));
+}
diff --git a/src/writer/html/commit.c b/src/writer/html/commit.c
@@ -15,6 +15,7 @@
struct HtmlCommit {
FILE* out;
+ const FileSystem* fs;
HtmlPage* page;
};
@@ -35,18 +36,23 @@ static void html_commit_write_diff_hunk(HtmlCommit* commit,
const GitHunk* hunk);
HtmlCommit* html_commit_create(const GitRepo* repo,
+ const FileSystem* fs,
const char* oid,
const char* title) {
HtmlCommit* commit = ecalloc(1, sizeof(HtmlCommit));
+ commit->fs = fs;
char filename[PATH_MAX];
int r = snprintf(filename, sizeof(filename), "%s.html", oid);
if (r < 0 || (size_t)r >= sizeof(filename)) {
errx(1, "snprintf: filename truncated or error");
}
char* path = path_concat("commit", filename);
- commit->out = efopen(path, "w");
+ commit->out = fs->fopen(path, "w");
+ if (!commit->out) {
+ err(1, "fopen: %s", path);
+ }
free(path);
- commit->page = html_page_create(commit->out, repo, title, "../");
+ commit->page = html_page_create(commit->out, repo, fs, title, "../");
return commit;
}
@@ -54,7 +60,7 @@ void html_commit_free(HtmlCommit* commit) {
if (!commit) {
return;
}
- fclose(commit->out);
+ commit->fs->fclose(commit->out);
commit->out = NULL;
html_page_free(commit->page);
commit->page = NULL;
@@ -94,8 +100,8 @@ void html_commit_end(HtmlCommit* commit) {
html_page_end(commit->page);
}
-void html_commit_write_summary(HtmlCommit* commit,
- const GitCommit* git_commit) {
+static void html_commit_write_summary(HtmlCommit* commit,
+ const GitCommit* git_commit) {
FILE* out = commit->out;
const char* oid = git_commit->oid;
fprintf(out, "<pre><b>commit</b> ");
@@ -134,8 +140,8 @@ void html_commit_write_summary(HtmlCommit* commit,
}
}
-void html_commit_write_diffstat(HtmlCommit* commit,
- const GitCommit* git_commit) {
+static void html_commit_write_diffstat(HtmlCommit* commit,
+ const GitCommit* git_commit) {
fprintf(commit->out, "<b>Diffstat:</b>\n<table>");
size_t delta_count = git_commit->deltas_len;
for (size_t i = 0; i < delta_count; i++) {
@@ -144,9 +150,9 @@ void html_commit_write_diffstat(HtmlCommit* commit,
fprintf(commit->out, "</table></pre>");
}
-void html_commit_write_diffstat_row(HtmlCommit* commit,
- size_t row,
- const GitDelta* delta) {
+static void html_commit_write_diffstat_row(HtmlCommit* commit,
+ size_t row,
+ const GitDelta* delta) {
static const size_t kGraphWidth = 78;
FILE* out = commit->out;
@@ -180,8 +186,8 @@ void html_commit_write_diffstat_row(HtmlCommit* commit,
fprintf(out, "</td></tr>\n");
}
-void html_commit_write_diff_content(HtmlCommit* commit,
- const GitCommit* git_commit) {
+static void html_commit_write_diff_content(HtmlCommit* commit,
+ const GitCommit* git_commit) {
FILE* out = commit->out;
size_t filecount = git_commit->filecount;
size_t addcount = git_commit->addcount;
@@ -200,9 +206,9 @@ void html_commit_write_diff_content(HtmlCommit* commit,
fprintf(out, "</pre>\n");
}
-void html_commit_write_diff_delta(HtmlCommit* commit,
- size_t file_num,
- const GitDelta* delta) {
+static void html_commit_write_diff_delta(HtmlCommit* commit,
+ size_t file_num,
+ const GitDelta* delta) {
const char* old_file_path = delta->old_file_path;
const char* new_file_path = delta->new_file_path;
fprintf(commit->out, "<b>diff --git ");
@@ -226,9 +232,9 @@ void html_commit_write_diff_delta(HtmlCommit* commit,
}
}
-void html_commit_write_diff_hunk(HtmlCommit* commit,
- size_t file_num,
- const GitHunk* hunk) {
+static void html_commit_write_diff_hunk(HtmlCommit* commit,
+ size_t file_num,
+ const GitHunk* hunk) {
FILE* out = commit->out;
// Output header. e.g. @@ -0,0 +1,3 @@
diff --git a/src/writer/html/commit.h b/src/writer/html/commit.h
@@ -3,10 +3,12 @@
#include "git/commit.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct HtmlCommit HtmlCommit;
HtmlCommit* html_commit_create(const GitRepo* repo,
+ const FileSystem* fs,
const char* oid,
const char* title);
void html_commit_free(HtmlCommit* commit);
diff --git a/src/writer/html/commit_tests.c b/src/writer/html/commit_tests.c
@@ -0,0 +1,125 @@
+#include "writer/html/commit.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/commit.h"
+#include "git/delta.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct html_commit {
+ int dummy;
+};
+
+UTEST_F_SETUP(html_commit) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(html_commit) {}
+
+UTEST_F(html_commit, basic) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlCommit* commit_writer =
+ html_commit_create(&repo, g_fs_inmemory, "sha123", "Commit Title");
+ ASSERT_NE(NULL, commit_writer);
+
+ GitCommit commit = {
+ .oid = "sha123",
+ .parentoid = "parent456",
+ .summary = "Fix a bug",
+ .message = "Detailed description.",
+ .author_name = "Author Name",
+ .author_email = "author@example.com",
+ .author_time = 1702031400,
+ .author_timezone_offset = 0,
+ };
+
+ html_commit_begin(commit_writer);
+ html_commit_add_commit(commit_writer, &commit);
+ html_commit_end(commit_writer);
+ html_commit_free(commit_writer);
+
+ const char* buf = inmemory_fs_get_buffer("commit/sha123.html");
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Header/Page Title */
+ EXPECT_STR_SEQUENCE(buf, "Commit Title");
+
+ /* Verify Author Info */
+ EXPECT_STR_SEQUENCE(buf, "Author Name", "author@example.com");
+
+ /* Verify Parent */
+ EXPECT_STR_SEQUENCE(buf, "parent", "parent456");
+
+ /* Verify Message */
+ EXPECT_STR_SEQUENCE(buf, "Detailed description.");
+}
+
+UTEST_F(html_commit, diff) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlCommit* commit_writer =
+ html_commit_create(&repo, g_fs_inmemory, "sha123", "Commit Title");
+
+ GitHunkLine l1 = {.id = 1,
+ .old_lineno = 1,
+ .new_lineno = 1,
+ .content = " unchanged",
+ .content_len = 10};
+ GitHunkLine l2 = {.id = 2,
+ .old_lineno = -1,
+ .new_lineno = 2,
+ .content = "added line",
+ .content_len = 10};
+ GitHunkLine l3 = {.id = 3,
+ .old_lineno = 2,
+ .new_lineno = -1,
+ .content = "removed line",
+ .content_len = 12};
+
+ GitHunkLine* lines[] = {&l1, &l2, &l3};
+ GitHunk hunk = {
+ .id = 1, .header = "@@ -1,2 +1,2 @@", .lines = lines, .lines_len = 3};
+ GitHunk* hunks[] = {&hunk};
+
+ GitDelta delta = {
+ .status = 'M',
+ .old_file_path = "file.txt",
+ .new_file_path = "file.txt",
+ .addcount = 1,
+ .delcount = 1,
+ .hunks = hunks,
+ .hunks_len = 1,
+ };
+ GitDelta* deltas[] = {&delta};
+
+ GitCommit commit = {
+ .oid = "sha123",
+ .parentoid = "",
+ .author_name = "User",
+ .author_email = "user@mail.com",
+ .deltas = deltas,
+ .deltas_len = 1,
+ .filecount = 1,
+ .addcount = 1,
+ .delcount = 1,
+ };
+
+ html_commit_begin(commit_writer);
+ html_commit_add_commit(commit_writer, &commit);
+ html_commit_end(commit_writer);
+ html_commit_free(commit_writer);
+
+ const char* buf = inmemory_fs_get_buffer("commit/sha123.html");
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Diffstat */
+ EXPECT_STR_SEQUENCE(buf, "Diffstat:", "file.txt", "2");
+
+ /* Verify Diff Content */
+ EXPECT_STR_SEQUENCE(buf, "diff --git", "file.txt", "@@ -1,2 +1,2 @@",
+ " unchanged", "+added line", "-removed line");
+}
diff --git a/src/writer/html/fileblob.c b/src/writer/html/fileblob.c
@@ -14,16 +14,20 @@
struct HtmlFileBlob {
const GitRepo* repo;
+ const FileSystem* fs;
FILE* out;
HtmlPage* page;
};
-HtmlFileBlob* html_fileblob_create(const GitRepo* repo, const char* path) {
+HtmlFileBlob* html_fileblob_create(const GitRepo* repo,
+ const FileSystem* fs,
+ const char* path) {
if (!is_safe_repo_path(path)) {
errx(1, "unsafe path: %s", path);
}
HtmlFileBlob* blob = ecalloc(1, sizeof(HtmlFileBlob));
blob->repo = repo;
+ blob->fs = fs;
// Create directories.
char filename_buffer[PATH_MAX];
@@ -38,8 +42,11 @@ HtmlFileBlob* html_fileblob_create(const GitRepo* repo, const char* path) {
if (!d) {
err(1, "dirname");
}
- mkdirp(d);
- blob->out = efopen(out_path, "w");
+ fs->mkdirp(d);
+ blob->out = fs->fopen(out_path, "w");
+ if (!blob->out) {
+ err(1, "fopen: %s", out_path);
+ }
// Compute the relative path.
char* relpath = relpath_from_dir(d);
@@ -50,7 +57,7 @@ HtmlFileBlob* html_fileblob_create(const GitRepo* repo, const char* path) {
if (!title) {
err(1, "basename");
}
- blob->page = html_page_create(blob->out, repo, title, relpath);
+ blob->page = html_page_create(blob->out, repo, fs, title, relpath);
free(out_path);
free(path_copy);
@@ -63,7 +70,7 @@ void html_fileblob_free(HtmlFileBlob* blob) {
if (!blob) {
return;
}
- fclose(blob->out);
+ blob->fs->fclose(blob->out);
blob->out = NULL;
html_page_free(blob->page);
blob->page = NULL;
diff --git a/src/writer/html/fileblob.h b/src/writer/html/fileblob.h
@@ -3,10 +3,13 @@
#include "git/file.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct HtmlFileBlob HtmlFileBlob;
-HtmlFileBlob* html_fileblob_create(const GitRepo* repo, const char* path);
+HtmlFileBlob* html_fileblob_create(const GitRepo* repo,
+ const FileSystem* fs,
+ const char* path);
void html_fileblob_free(HtmlFileBlob* blob);
void html_fileblob_begin(HtmlFileBlob* blob);
void html_fileblob_add_file(HtmlFileBlob* blob, const GitFile* file);
diff --git a/src/writer/html/fileblob_tests.c b/src/writer/html/fileblob_tests.c
@@ -0,0 +1,118 @@
+#include "writer/html/fileblob.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/file.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct html_fileblob {
+ int dummy;
+};
+
+UTEST_F_SETUP(html_fileblob) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(html_fileblob) {}
+
+UTEST_F(html_fileblob, basic) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlFileBlob* blob_writer =
+ html_fileblob_create(&repo, g_fs_inmemory, "src/main.c");
+ ASSERT_NE(NULL, blob_writer);
+
+ const char* content = "int main() {\n return 0;\n}\n";
+ GitFile file = {
+ .repo_path = "src/main.c",
+ .content = (char*)content,
+ .size_bytes = strlen(content),
+ .size_lines = 3,
+ };
+
+ html_fileblob_begin(blob_writer);
+ html_fileblob_add_file(blob_writer, &file);
+ html_fileblob_end(blob_writer);
+ html_fileblob_free(blob_writer);
+
+ const char* buf = inmemory_fs_get_buffer("file/src/main.c.html");
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Header/Filename */
+ EXPECT_STR_SEQUENCE(buf, "main.c", "(27B)");
+
+ /* Verify Content and Line Numbers */
+ EXPECT_STR_SEQUENCE(buf, "id=\"l1\"", " 1", "int main()", //
+ "id=\"l2\"", " 2", "return 0;", //
+ "id=\"l3\"", " 3", "}");
+}
+
+UTEST_F(html_fileblob, binary) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlFileBlob* blob_writer =
+ html_fileblob_create(&repo, g_fs_inmemory, "logo.png");
+
+ GitFile file = {
+ .repo_path = "logo.png",
+ .size_bytes = 100,
+ .size_lines = -1, /* Binary */
+ };
+
+ html_fileblob_begin(blob_writer);
+ html_fileblob_add_file(blob_writer, &file);
+ html_fileblob_end(blob_writer);
+ html_fileblob_free(blob_writer);
+
+ const char* buf = inmemory_fs_get_buffer("file/logo.png.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "logo.png", "Binary file.");
+}
+
+UTEST_F(html_fileblob, too_large) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlFileBlob* blob_writer =
+ html_fileblob_create(&repo, g_fs_inmemory, "big.txt");
+
+ GitFile file = {
+ .repo_path = "big.txt",
+ .size_bytes = 1000000,
+ .size_lines = -2, /* Too large */
+ };
+
+ html_fileblob_begin(blob_writer);
+ html_fileblob_add_file(blob_writer, &file);
+ html_fileblob_end(blob_writer);
+ html_fileblob_free(blob_writer);
+
+ const char* buf = inmemory_fs_get_buffer("file/big.txt.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "big.txt", "File too large to display.");
+}
+
+UTEST_F(html_fileblob, escaping) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlFileBlob* blob_writer =
+ html_fileblob_create(&repo, g_fs_inmemory, "tags.html");
+
+ const char* content = "<html>\n";
+ GitFile file = {
+ .repo_path = "tags.html",
+ .content = (char*)content,
+ .size_bytes = strlen(content),
+ .size_lines = 1,
+ };
+
+ html_fileblob_begin(blob_writer);
+ html_fileblob_add_file(blob_writer, &file);
+ html_fileblob_end(blob_writer);
+ html_fileblob_free(blob_writer);
+
+ const char* buf = inmemory_fs_get_buffer("file/tags.html.html");
+ ASSERT_NE(NULL, buf);
+ /* Verify that <html> is escaped to <html> */
+ EXPECT_STR_SEQUENCE(buf, "<html>");
+}
diff --git a/src/writer/html/files.c b/src/writer/html/files.c
@@ -1,5 +1,6 @@
#include "writer/html/files.h"
+#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
@@ -10,15 +11,20 @@
struct HtmlFiles {
const GitRepo* repo;
+ const FileSystem* fs;
FILE* out;
HtmlPage* page;
};
-HtmlFiles* html_files_create(const GitRepo* repo) {
+HtmlFiles* html_files_create(const GitRepo* repo, const FileSystem* fs) {
HtmlFiles* files = ecalloc(1, sizeof(HtmlFiles));
files->repo = repo;
- files->out = efopen("files.html", "w");
- files->page = html_page_create(files->out, repo, "Files", "");
+ files->fs = fs;
+ files->out = fs->fopen("files.html", "w");
+ if (!files->out) {
+ err(1, "fopen: files.html");
+ }
+ files->page = html_page_create(files->out, repo, fs, "Files", "");
return files;
}
@@ -26,7 +32,7 @@ void html_files_free(HtmlFiles* files) {
if (!files) {
return;
}
- fclose(files->out);
+ files->fs->fclose(files->out);
files->out = NULL;
html_page_free(files->page);
files->page = NULL;
diff --git a/src/writer/html/files.h b/src/writer/html/files.h
@@ -3,10 +3,11 @@
#include "git/file.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct HtmlFiles HtmlFiles;
-HtmlFiles* html_files_create(const GitRepo* repo);
+HtmlFiles* html_files_create(const GitRepo* repo, const FileSystem* fs);
void html_files_free(HtmlFiles* files);
void html_files_begin(HtmlFiles* files);
void html_files_add_file(HtmlFiles* files, const GitFile* file);
diff --git a/src/writer/html/files_tests.c b/src/writer/html/files_tests.c
@@ -0,0 +1,97 @@
+#include "writer/html/files.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/file.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct html_files {
+ int dummy;
+};
+
+UTEST_F_SETUP(html_files) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(html_files) {}
+
+UTEST_F(html_files, basic) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlFiles* files = html_files_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, files);
+
+ GitFile f1 = {
+ .type = kFileTypeFile,
+ .mode = "-rw-r--r--",
+ .display_path = "README.md",
+ .repo_path = "README.md",
+ .size_bytes = 100,
+ .size_lines = 5,
+ .commit_oid = "",
+ };
+
+ html_files_begin(files);
+ html_files_add_file(files, &f1);
+ html_files_end(files);
+ html_files_free(files);
+
+ const char* buf = inmemory_fs_get_buffer("files.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "<thead>", "Mode", "Name", "Size", "</thead>",
+ "<tbody>", "-rw-r--r--", "README.md", "5L", "</tbody>");
+}
+
+UTEST_F(html_files, submodule) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlFiles* files = html_files_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, files);
+
+ GitFile sub = {
+ .type = kFileTypeSubmodule,
+ .mode = "m---------",
+ .display_path = "third_party/lib",
+ .repo_path = "third_party/lib",
+ .size_bytes = -1,
+ .size_lines = -1,
+ .commit_oid = "abcdef123456",
+ };
+
+ html_files_begin(files);
+ html_files_add_file(files, &sub);
+ html_files_end(files);
+ html_files_free(files);
+
+ const char* buf = inmemory_fs_get_buffer("files.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "m---------", "third_party/lib", "@ abcdef123456");
+}
+
+UTEST_F(html_files, binary_file) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlFiles* files = html_files_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, files);
+
+ GitFile bin = {
+ .type = kFileTypeFile,
+ .mode = "-rwxr-xr-x",
+ .display_path = "logo.png",
+ .repo_path = "logo.png",
+ .size_bytes = 1024,
+ .size_lines = -1, /* -1 indicates binary */
+ .commit_oid = "",
+ };
+
+ html_files_begin(files);
+ html_files_add_file(files, &bin);
+ html_files_end(files);
+ html_files_free(files);
+
+ const char* buf = inmemory_fs_get_buffer("files.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "-rwxr-xr-x", "logo.png", "1024B");
+}
diff --git a/src/writer/html/index_writer.c b/src/writer/html/index_writer.c
@@ -11,9 +11,8 @@ struct HtmlIndexWriter {
HtmlRepoIndex* index;
};
-HtmlIndexWriter* html_indexwriter_create(void) {
+HtmlIndexWriter* html_indexwriter_create(FILE* out) {
HtmlIndexWriter* writer = ecalloc(1, sizeof(HtmlIndexWriter));
- FILE* out = stdout;
writer->index = html_repoindex_create(out);
return writer;
}
@@ -35,7 +34,7 @@ void html_indexwriter_begin(HtmlIndexWriter* writer) {
html_repoindex_begin(writer->index);
}
-void html_indexwriter_add_repo(HtmlIndexWriter* writer, GitRepo* repo) {
+void html_indexwriter_add_repo(HtmlIndexWriter* writer, const GitRepo* repo) {
html_repoindex_add_repo(writer->index, repo);
}
diff --git a/src/writer/html/index_writer.h b/src/writer/html/index_writer.h
@@ -3,13 +3,15 @@
#include "git/repo.h"
+#include <stdio.h>
+
typedef struct HtmlIndexWriter HtmlIndexWriter;
-HtmlIndexWriter* html_indexwriter_create(void);
+HtmlIndexWriter* html_indexwriter_create(FILE* out);
void html_indexwriter_free(HtmlIndexWriter* writer);
void html_indexwriter_set_me_url(HtmlIndexWriter* writer, const char* url);
void html_indexwriter_begin(HtmlIndexWriter* writer);
-void html_indexwriter_add_repo(HtmlIndexWriter* writer, GitRepo* repo);
+void html_indexwriter_add_repo(HtmlIndexWriter* writer, const GitRepo* repo);
void html_indexwriter_end(HtmlIndexWriter* writer);
#endif // GOUT_WRITER_HTML_INDEX_WRITER_H_
diff --git a/src/writer/html/log.c b/src/writer/html/log.c
@@ -8,10 +8,19 @@
#include "writer/cache/cache.h"
#include "writer/html/page.h"
+#include <err.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
struct HtmlLog {
const GitRepo* repo;
+ const FileSystem* fs;
FILE* out;
Cache* cache;
+ char* cache_path;
+ char* temp_cache_path;
+ FILE* cache_in;
+ FILE* cache_out;
HtmlPage* page;
size_t remaining_commits;
size_t unlogged_commits;
@@ -19,11 +28,19 @@ struct HtmlLog {
static void write_commit_row(FILE* out, const GitCommit* commit);
-HtmlLog* html_log_create(const GitRepo* repo) {
+static const char* kTempCachePath = "cache.XXXXXXXXXXXX";
+static const mode_t kReadWriteAll =
+ S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
+
+HtmlLog* html_log_create(const GitRepo* repo, const FileSystem* fs) {
HtmlLog* log = ecalloc(1, sizeof(HtmlLog));
log->repo = repo;
- log->out = efopen("log.html", "w");
- log->page = html_page_create(log->out, repo, "Log", "");
+ log->fs = fs;
+ log->out = fs->fopen("log.html", "w");
+ if (!log->out) {
+ err(1, "fopen: log.html");
+ }
+ log->page = html_page_create(log->out, repo, fs, "Log", "");
log->remaining_commits = SIZE_MAX;
log->unlogged_commits = 0;
return log;
@@ -33,17 +50,41 @@ void html_log_free(HtmlLog* log) {
if (!log) {
return;
}
- fclose(log->out);
+ log->fs->fclose(log->out);
log->out = NULL;
cache_free(log->cache);
log->cache = NULL;
+ free(log->cache_path);
+ free(log->temp_cache_path);
+ if (log->cache_in) {
+ log->fs->fclose(log->cache_in);
+ }
+ if (log->cache_out) {
+ /* TODO: When fdopen is added to FileSystem, update to fs->fclose. */
+ fclose(log->cache_out);
+ }
html_page_free(log->page);
log->page = NULL;
free(log);
}
void html_log_set_cachefile(HtmlLog* log, const char* cachefile) {
- log->cache = cache_create(cachefile, write_commit_row);
+ log->cache_path = estrdup(cachefile);
+ log->temp_cache_path = estrdup(kTempCachePath);
+
+ log->cache_in = log->fs->fopen(cachefile, "r");
+ int out_fd = log->fs->mkstemp(log->temp_cache_path);
+ if (out_fd == -1) {
+ err(1, "mkstemp: %s", log->temp_cache_path);
+ }
+ /* TODO: Consider adding fdopen to the FileSystem VTable. */
+ log->cache_out = fdopen(out_fd, "w");
+ if (!log->cache_out) {
+ close(out_fd);
+ err(1, "fdopen: %s", log->temp_cache_path);
+ }
+
+ log->cache = cache_create(log->cache_in, log->cache_out, write_commit_row);
}
void html_log_set_commit_limit(HtmlLog* log, size_t count) {
@@ -83,8 +124,31 @@ void html_log_add_commit(HtmlLog* log, const GitCommit* commit) {
void html_log_end(HtmlLog* log) {
FILE* out = log->out;
if (log->cache) {
- cache_write(log->cache);
- cache_copy_log(log->cache, log->out);
+ cache_finish(log->cache);
+ if (log->cache_in) {
+ log->fs->fclose(log->cache_in);
+ log->cache_in = NULL;
+ }
+ /* TODO: Use fs->fclose() for consistency. */
+ fclose(log->cache_out);
+ log->cache_out = NULL;
+
+ if (log->fs->rename(log->temp_cache_path, log->cache_path)) {
+ err(1, "rename: %s -> %s", log->temp_cache_path, log->cache_path);
+ }
+
+ mode_t mask;
+ umask(mask = umask(0));
+ if (log->fs->chmod(log->cache_path, kReadWriteAll & ~mask)) {
+ err(1, "chmod: %s", log->cache_path);
+ }
+
+ FILE* fcache = log->fs->fopen(log->cache_path, "r");
+ if (!fcache) {
+ err(1, "fopen: %s", log->cache_path);
+ }
+ cache_copy_log(fcache, log->out);
+ log->fs->fclose(fcache);
} else if (log->unlogged_commits > 0) {
size_t count = log->unlogged_commits;
fprintf(out, "<tr><td></td><td colspan=\"5\">");
@@ -95,7 +159,7 @@ void html_log_end(HtmlLog* log) {
html_page_end(log->page);
}
-void write_commit_row(FILE* out, const GitCommit* commit) {
+static void write_commit_row(FILE* out, const GitCommit* commit) {
fprintf(out, "<tr><td>");
print_time_short(out, commit->author_time);
fprintf(out, "</td><td>");
diff --git a/src/writer/html/log.h b/src/writer/html/log.h
@@ -6,10 +6,11 @@
#include "git/commit.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct HtmlLog HtmlLog;
-HtmlLog* html_log_create(const GitRepo* repo);
+HtmlLog* html_log_create(const GitRepo* repo, const FileSystem* fs);
void html_log_free(HtmlLog* log);
void html_log_set_cachefile(HtmlLog* log, const char* cachefile);
void html_log_set_commit_limit(HtmlLog* log, size_t count);
diff --git a/src/writer/html/log_tests.c b/src/writer/html/log_tests.c
@@ -0,0 +1,123 @@
+#include "writer/html/log.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/commit.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct html_log {
+ int dummy;
+};
+
+UTEST_F_SETUP(html_log) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(html_log) {}
+
+UTEST_F(html_log, begin) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlLog* log = html_log_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, log);
+ html_log_begin(log);
+ html_log_free(log);
+
+ const char* buf = inmemory_fs_get_buffer("log.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "test-repo", "<thead>", "Date", "Commit message",
+ "Author");
+}
+
+UTEST_F(html_log, add_commit) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlLog* log = html_log_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, log);
+
+ GitCommit commit = {
+ .oid = "abc1234",
+ .summary = "Fix bug",
+ .author_name = "User",
+ .author_time = 1702031400,
+ .filecount = 2,
+ .addcount = 10,
+ .delcount = 5,
+ };
+
+ html_log_begin(log);
+ html_log_add_commit(log, &commit);
+ html_log_end(log);
+ html_log_free(log);
+
+ const char* buf = inmemory_fs_get_buffer("log.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "2023-12-08 10:30", "Fix bug", "User", "2", "+10",
+ "-5");
+}
+
+UTEST_F(html_log, commit_limit) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlLog* log = html_log_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, log);
+ html_log_set_commit_limit(log, 2);
+
+ GitCommit commit = {.oid = "sha", .author_name = "user", .summary = "msg"};
+
+ html_log_begin(log);
+ html_log_add_commit(log, &commit);
+ html_log_add_commit(log, &commit);
+ html_log_add_commit(log, &commit); /* Should be unlogged */
+
+ html_log_end(log);
+ html_log_free(log);
+
+ const char* buf = inmemory_fs_get_buffer("log.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_COUNT(buf, "msg", 2);
+ EXPECT_NE(NULL, strstr(buf, "1 more commits remaining"));
+}
+
+UTEST_F(html_log, cache_integration) {
+ /* 1. Setup a "previous" cache.
+ * The first line is the OID, the rest is the log rows. */
+ const char* old_cache_content = "sha_old\n<tr><td>Old Commit</td></tr>\n";
+
+ /* We need to pre-register this file in our mock FS so it can be read.
+ * Since our mock fopen('r') looks at g_files, we can 'write' it first. */
+ FILE* pre = g_fs_inmemory->fopen("cache.db", "w");
+ fprintf(pre, "%s", old_cache_content);
+ g_fs_inmemory->fclose(pre);
+
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlLog* log = html_log_create(&repo, g_fs_inmemory);
+ html_log_set_cachefile(log, "cache.db");
+
+ GitCommit c_new = {
+ .oid = "sha_new", .summary = "New Commit", .author_name = "User"};
+ GitCommit c_old = {
+ .oid = "sha_old", .summary = "Old Commit", .author_name = "User"};
+
+ html_log_begin(log);
+ html_log_add_commit(log, &c_new);
+ html_log_add_commit(log, &c_old); /* This should trigger the cache stop */
+ html_log_end(log);
+ html_log_free(log);
+
+ /* Verify log.html contains BOTH the new commit and the OLD cache content. */
+ const char* buf = inmemory_fs_get_buffer("log.html");
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_STR_SEQUENCE(
+ buf, "New Commit",
+ "Old Commit" /* This comes from the cache_copy_log call in html_log_end */
+ );
+
+ /* Verify the cache file itself was updated with the new OID. */
+ const char* cache_buf = inmemory_fs_get_buffer("cache.db");
+ ASSERT_NE(NULL, cache_buf);
+ EXPECT_TRUE(strncmp(cache_buf, "sha_new", 7) == 0);
+}
+\ No newline at end of file
diff --git a/src/writer/html/page.c b/src/writer/html/page.c
@@ -8,17 +8,20 @@
struct HtmlPage {
FILE* out;
const GitRepo* repo;
+ const FileSystem* fs;
char* title;
char* relpath;
};
HtmlPage* html_page_create(FILE* out,
const GitRepo* repo,
+ const FileSystem* fs,
const char* title,
const char* relpath) {
HtmlPage* page = ecalloc(1, sizeof(HtmlPage));
page->out = out;
page->repo = repo;
+ page->fs = fs;
page->title = estrdup(title);
page->relpath = estrdup(relpath);
return page;
@@ -49,12 +52,12 @@ void html_page_begin(HtmlPage* page) {
fprintf(out, "<title>");
print_xml_encoded(out, page->title);
- const char* short_name = gitrepo_short_name(page->repo);
+ const char* short_name = page->repo->short_name;
if (page->title[0] != '\0' && short_name && short_name[0] != '\0') {
fprintf(out, " - ");
print_xml_encoded(out, short_name);
}
- const char* description = gitrepo_description(page->repo);
+ const char* description = page->repo->description;
if (page->title[0] != '\0' && description && description[0] != '\0') {
fprintf(out, " - ");
print_xml_encoded(out, description);
@@ -67,11 +70,15 @@ void html_page_begin(HtmlPage* page) {
relpath);
fprintf(out,
"<link rel=\"alternate\" type=\"application/atom+xml\" title=\"");
- print_xml_encoded(out, gitrepo_name(page->repo));
+ if (page->repo->name) {
+ print_xml_encoded(out, page->repo->name);
+ }
fprintf(out, " Atom Feed\" href=\"%satom.xml\" />\n", relpath);
fprintf(out,
"<link rel=\"alternate\" type=\"application/atom+xml\" title=\"");
- print_xml_encoded(out, gitrepo_name(page->repo));
+ if (page->repo->name) {
+ print_xml_encoded(out, page->repo->name);
+ }
fprintf(out, " Atom Feed (tags)\" href=\"%stags.xml\" />\n", relpath);
fprintf(out,
"<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />",
@@ -81,12 +88,16 @@ void html_page_begin(HtmlPage* page) {
fprintf(out, "<img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" />",
relpath);
fprintf(out, "</a></td><td><h1>");
- print_xml_encoded(out, gitrepo_short_name(page->repo));
+ if (page->repo->short_name) {
+ print_xml_encoded(out, page->repo->short_name);
+ }
fprintf(out, "</h1><span class=\"desc\">");
- print_xml_encoded(out, gitrepo_description(page->repo));
+ if (page->repo->description) {
+ print_xml_encoded(out, page->repo->description);
+ }
fprintf(out, "</span></td></tr>");
- const char* clone_url = gitrepo_clone_url(page->repo);
+ const char* clone_url = page->repo->clone_url;
if (clone_url && clone_url[0] != '\0') {
fprintf(out, "<tr class=\"url\"><td></td><td>git clone ");
if (is_safe_url(clone_url)) {
@@ -105,19 +116,19 @@ void html_page_begin(HtmlPage* page) {
fprintf(out, "<a href=\"%sfiles.html\">Files</a> | ", relpath);
fprintf(out, "<a href=\"%srefs.html\">Refs</a>", relpath);
- const char* submodules = gitrepo_submodules(page->repo);
+ const char* submodules = page->repo->submodules;
if (submodules && submodules[0] != '\0') {
fprintf(out, " | <a href=\"%sfile/", relpath);
print_xml_encoded(out, submodules);
fprintf(out, ".html\">Submodules</a>");
}
- const char* readme = gitrepo_readme(page->repo);
+ const char* readme = page->repo->readme;
if (readme && readme[0] != '\0') {
fprintf(out, " | <a href=\"%sfile/", relpath);
print_xml_encoded(out, readme);
fprintf(out, ".html\">README</a>");
}
- const char* license = gitrepo_license(page->repo);
+ const char* license = page->repo->license;
if (license && license[0] != '\0') {
fprintf(out, " | <a href=\"%sfile/", relpath);
print_xml_encoded(out, license);
diff --git a/src/writer/html/page.h b/src/writer/html/page.h
@@ -4,11 +4,13 @@
#include <stdio.h>
#include "git/repo.h"
+#include "utils.h"
typedef struct HtmlPage HtmlPage;
HtmlPage* html_page_create(FILE* out,
const GitRepo* repo,
+ const FileSystem* fs,
const char* title,
const char* relpath);
void html_page_free(HtmlPage* page);
diff --git a/src/writer/html/page_tests.c b/src/writer/html/page_tests.c
@@ -0,0 +1,74 @@
+#include "writer/html/page.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct html_page {
+ int dummy;
+};
+
+UTEST_F_SETUP(html_page) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(html_page) {}
+
+UTEST_F(html_page, basic) {
+ GitRepo repo = {
+ .short_name = "test-repo",
+ .description = "Repo description",
+ .clone_url = "git://example.com/repo.git",
+ .license = "LICENSE",
+ };
+
+ FILE* out = g_fs_inmemory->fopen("test.html", "w");
+ HtmlPage* page =
+ html_page_create(out, &repo, g_fs_inmemory, "Page Title", "../");
+ ASSERT_NE(NULL, page);
+
+ html_page_begin(page);
+ html_page_end(page);
+ g_fs_inmemory->fclose(out);
+ html_page_free(page);
+
+ const char* buf = inmemory_fs_get_buffer("test.html");
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Header/Title */
+ EXPECT_STR_SEQUENCE(buf, "<title>", "Page Title", "test-repo",
+ "Repo description", "</title>");
+
+ /* Verify Navigation */
+ EXPECT_STR_SEQUENCE(buf, "href=\"../log.html\">Log</a>",
+ "href=\"../files.html\">Files</a>",
+ "href=\"../refs.html\">Refs</a>");
+
+ /* Verify Metadata */
+ EXPECT_STR_SEQUENCE(buf, "git clone", "git://example.com/repo.git",
+ "LICENSE");
+}
+
+UTEST_F(html_page, unsafe_url) {
+ GitRepo repo = {
+ .clone_url = "javascript:alert(1)",
+ };
+
+ FILE* out = g_fs_inmemory->fopen("unsafe.html", "w");
+ HtmlPage* page = html_page_create(out, &repo, g_fs_inmemory, "Title", "");
+ ASSERT_NE(NULL, page);
+ html_page_begin(page);
+ g_fs_inmemory->fclose(out);
+ html_page_free(page);
+
+ const char* buf = inmemory_fs_get_buffer("unsafe.html");
+ ASSERT_NE(NULL, buf);
+ /* Verify that the unsafe URL is NOT turned into an <a> tag. */
+ EXPECT_NE(NULL, strstr(buf, "javascript:alert(1)"));
+ EXPECT_EQ(NULL, strstr(buf, "href=\"javascript:alert(1)\""));
+}
diff --git a/src/writer/html/refs.c b/src/writer/html/refs.c
@@ -1,5 +1,6 @@
#include "writer/html/refs.h"
+#include <err.h>
#include <stdio.h>
#include <stdlib.h>
@@ -16,6 +17,7 @@ typedef struct {
struct HtmlRefs {
const GitRepo* repo;
+ const FileSystem* fs;
FILE* out;
HtmlPage* page;
HtmlRefsTable* branches;
@@ -31,9 +33,9 @@ static void html_refstable_add_ref(HtmlRefsTable* table,
const GitReference* ref);
static void html_refstable_end(HtmlRefsTable* table);
-HtmlRefsTable* html_refstable_create(const char* title,
- const char* id,
- FILE* out) {
+static HtmlRefsTable* html_refstable_create(const char* title,
+ const char* id,
+ FILE* out) {
HtmlRefsTable* table = ecalloc(1, sizeof(HtmlRefsTable));
table->title = estrdup(title);
table->id = estrdup(id);
@@ -41,7 +43,7 @@ HtmlRefsTable* html_refstable_create(const char* title,
return table;
}
-void html_refstable_free(HtmlRefsTable* table) {
+static void html_refstable_free(HtmlRefsTable* table) {
if (!table) {
return;
}
@@ -52,7 +54,7 @@ void html_refstable_free(HtmlRefsTable* table) {
free(table);
}
-void html_refstable_begin(HtmlRefsTable* table) {
+static void html_refstable_begin(HtmlRefsTable* table) {
fprintf(table->out, "<h2>%s</h2>", table->title);
fprintf(table->out, "<table id=\"%s\">", table->id);
fprintf(table->out,
@@ -65,7 +67,8 @@ void html_refstable_begin(HtmlRefsTable* table) {
"</thead><tbody>\n");
}
-void html_refstable_add_ref(HtmlRefsTable* table, const GitReference* ref) {
+static void html_refstable_add_ref(HtmlRefsTable* table,
+ const GitReference* ref) {
GitCommit* commit = ref->commit;
fprintf(table->out, "<tr><td>");
print_xml_encoded(table->out, ref->shorthand);
@@ -76,15 +79,19 @@ void html_refstable_add_ref(HtmlRefsTable* table, const GitReference* ref) {
fprintf(table->out, "</td></tr>\n");
}
-void html_refstable_end(HtmlRefsTable* table) {
+static void html_refstable_end(HtmlRefsTable* table) {
fprintf(table->out, "</tbody></table><br/>\n");
}
-HtmlRefs* html_refs_create(const GitRepo* repo) {
+HtmlRefs* html_refs_create(const GitRepo* repo, const FileSystem* fs) {
HtmlRefs* refs = ecalloc(1, sizeof(HtmlRefs));
refs->repo = repo;
- refs->out = efopen("refs.html", "w");
- refs->page = html_page_create(refs->out, repo, "Refs", "");
+ refs->fs = fs;
+ refs->out = fs->fopen("refs.html", "w");
+ if (!refs->out) {
+ err(1, "fopen: refs.html");
+ }
+ refs->page = html_page_create(refs->out, repo, fs, "Refs", "");
return refs;
}
@@ -92,7 +99,7 @@ void html_refs_free(HtmlRefs* refs) {
if (!refs) {
return;
}
- fclose(refs->out);
+ refs->fs->fclose(refs->out);
refs->out = NULL;
html_page_free(refs->page);
refs->page = NULL;
diff --git a/src/writer/html/refs.h b/src/writer/html/refs.h
@@ -3,10 +3,11 @@
#include "git/reference.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct HtmlRefs HtmlRefs;
-HtmlRefs* html_refs_create(const GitRepo* repo);
+HtmlRefs* html_refs_create(const GitRepo* repo, const FileSystem* fs);
void html_refs_free(HtmlRefs* refs);
void html_refs_begin(HtmlRefs* refs);
void html_refs_add_ref(HtmlRefs* refs, const GitReference* ref);
diff --git a/src/writer/html/refs_tests.c b/src/writer/html/refs_tests.c
@@ -0,0 +1,98 @@
+#include "writer/html/refs.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/commit.h"
+#include "git/reference.h"
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+struct html_refs {
+ int dummy;
+};
+
+UTEST_F_SETUP(html_refs) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(html_refs) {}
+
+UTEST_F(html_refs, branches) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlRefs* refs = html_refs_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, refs);
+
+ GitCommit commit = {
+ .author_name = "User A",
+ .author_time = 1702031400,
+ };
+ GitReference ref = {
+ .type = kReftypeBranch,
+ .shorthand = "main",
+ .commit = &commit,
+ };
+
+ html_refs_begin(refs);
+ html_refs_add_ref(refs, &ref);
+ html_refs_end(refs);
+ html_refs_free(refs);
+
+ const char* buf = inmemory_fs_get_buffer("refs.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "<h2>Branches</h2>", "<table id=\"branches\">",
+ "main", "2023-12-08 10:30", "User A");
+}
+
+UTEST_F(html_refs, tags) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlRefs* refs = html_refs_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, refs);
+
+ GitCommit commit = {
+ .author_name = "User B",
+ .author_time = 1702035000,
+ };
+ GitReference ref = {
+ .type = kReftypeTag,
+ .shorthand = "v1.0",
+ .commit = &commit,
+ };
+
+ html_refs_begin(refs);
+ html_refs_add_ref(refs, &ref);
+ html_refs_end(refs);
+ html_refs_free(refs);
+
+ const char* buf = inmemory_fs_get_buffer("refs.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "<h2>Tags</h2>", "<table id=\"tags\">", "v1.0",
+ "2023-12-08 11:30", "User B");
+}
+
+UTEST_F(html_refs, both) {
+ GitRepo repo = {.short_name = "test-repo"};
+ HtmlRefs* refs = html_refs_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, refs);
+
+ GitCommit commit1 = {.author_name = "User 1", .author_time = 1000};
+ GitReference branch = {
+ .type = kReftypeBranch, .shorthand = "main", .commit = &commit1};
+
+ GitCommit commit2 = {.author_name = "User 2", .author_time = 2000};
+ GitReference tag = {
+ .type = kReftypeTag, .shorthand = "v1", .commit = &commit2};
+
+ html_refs_begin(refs);
+ html_refs_add_ref(refs, &branch);
+ html_refs_add_ref(refs, &tag);
+ html_refs_end(refs);
+ html_refs_free(refs);
+
+ const char* buf = inmemory_fs_get_buffer("refs.html");
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "<h2>Branches</h2>", "main", "<h2>Tags</h2>", "v1");
+}
diff --git a/src/writer/html/repo_index.c b/src/writer/html/repo_index.c
@@ -3,7 +3,6 @@
#include <stdlib.h>
#include "format.h"
-#include "git/commit.h"
#include "utils.h"
struct HtmlRepoIndex {
@@ -22,7 +21,6 @@ void html_repoindex_free(HtmlRepoIndex* index) {
if (!index) {
return;
}
- fclose(index->out);
index->out = NULL;
free(index);
}
@@ -64,23 +62,20 @@ void html_repoindex_begin(HtmlRepoIndex* index) {
"</thead><tbody>\n");
}
-static void print_author_time(const GitCommit* commit, void* user_data) {
- FILE* out = (FILE*)user_data;
- print_time_short(out, commit->author_time);
-}
-
-void html_repoindex_add_repo(HtmlRepoIndex* index, GitRepo* repo) {
+void html_repoindex_add_repo(HtmlRepoIndex* index, const GitRepo* repo) {
FILE* out = index->out;
fprintf(out, "<tr><td><a href=\"");
- print_percent_encoded(out, gitrepo_short_name(repo));
+ print_percent_encoded(out, repo->short_name);
fprintf(out, "/log.html\">");
- print_xml_encoded(out, gitrepo_short_name(repo));
+ print_xml_encoded(out, repo->short_name);
fprintf(out, "</a></td><td>");
- print_xml_encoded(out, gitrepo_description(repo));
+ print_xml_encoded(out, repo->description);
fprintf(out, "</td><td>");
- print_xml_encoded(out, gitrepo_owner(repo));
+ print_xml_encoded(out, repo->owner);
fprintf(out, "</td><td>");
- gitrepo_for_commit(repo, "HEAD", print_author_time, out);
+ if (repo->last_commit_time > 0) {
+ print_time_short(out, repo->last_commit_time);
+ }
fprintf(out, "</td></tr>");
}
diff --git a/src/writer/html/repo_index.h b/src/writer/html/repo_index.h
@@ -11,7 +11,7 @@ HtmlRepoIndex* html_repoindex_create(FILE* out);
void html_repoindex_free(HtmlRepoIndex* index);
void html_repoindex_set_me_url(HtmlRepoIndex* index, const char* url);
void html_repoindex_begin(HtmlRepoIndex* index);
-void html_repoindex_add_repo(HtmlRepoIndex* index, GitRepo* repo);
+void html_repoindex_add_repo(HtmlRepoIndex* index, const GitRepo* repo);
void html_repoindex_end(HtmlRepoIndex* index);
#endif // GOUT_WRITER_HTML_REPOINDEX_H_
diff --git a/src/writer/html/repo_index_tests.c b/src/writer/html/repo_index_tests.c
@@ -0,0 +1,67 @@
+#include "writer/html/repo_index.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "git/repo.h"
+#include "test_utils.h"
+#include "utest.h"
+
+UTEST(html_repo_index, basic) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+
+ HtmlRepoIndex* index = html_repoindex_create(out);
+ ASSERT_NE(NULL, index);
+ html_repoindex_set_me_url(index, "https://me.example.com");
+
+ GitRepo r1 = {
+ .short_name = "repo1",
+ .description = "Desc 1",
+ .owner = "Owner 1",
+ .last_commit_time = 1702031400,
+ };
+
+ html_repoindex_begin(index);
+ html_repoindex_add_repo(index, &r1);
+ html_repoindex_end(index);
+ html_repoindex_free(index);
+ fclose(out);
+
+ ASSERT_NE(NULL, buf);
+
+ /* Verify Header/Metadata */
+ EXPECT_STR_SEQUENCE(buf, "Repositories", "rel=\"me\"",
+ "https://me.example.com");
+
+ /* Verify Repo Row */
+ EXPECT_STR_SEQUENCE(buf, "repo1/log.html", "repo1", "Desc 1", "Owner 1",
+ "2023-12-08 10:30");
+
+ free(buf);
+}
+
+UTEST(html_repo_index, multiple) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+
+ HtmlRepoIndex* index = html_repoindex_create(out);
+
+ GitRepo r1 = {.short_name = "a", .description = "d1", .owner = "o1"};
+ GitRepo r2 = {.short_name = "b", .description = "d2", .owner = "o2"};
+
+ html_repoindex_begin(index);
+ html_repoindex_add_repo(index, &r1);
+ html_repoindex_add_repo(index, &r2);
+ html_repoindex_end(index);
+ html_repoindex_free(index);
+ fclose(out);
+
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "a/log.html", "d1", "o1", "b/log.html", "d2", "o2");
+
+ free(buf);
+}
diff --git a/src/writer/html/repo_writer.c b/src/writer/html/repo_writer.c
@@ -20,21 +20,34 @@
struct HtmlRepoWriter {
const GitRepo* repo;
+ const FileSystem* fs;
HtmlRefs* refs;
HtmlLog* log;
Atom* atom;
+ FILE* atom_out;
Atom* tags;
+ FILE* tags_out;
HtmlFiles* files;
};
-HtmlRepoWriter* html_repowriter_create(const GitRepo* repo) {
+HtmlRepoWriter* html_repowriter_create(const GitRepo* repo,
+ const FileSystem* fs) {
HtmlRepoWriter* writer = ecalloc(1, sizeof(HtmlRepoWriter));
writer->repo = repo;
- writer->refs = html_refs_create(repo);
- writer->log = html_log_create(repo);
- writer->atom = atom_create(repo, kAtomTypeAll);
- writer->tags = atom_create(repo, kAtomTypeTags);
- writer->files = html_files_create(repo);
+ writer->fs = fs;
+ writer->refs = html_refs_create(repo, fs);
+ writer->log = html_log_create(repo, fs);
+ writer->atom_out = fs->fopen("atom.xml", "w");
+ if (!writer->atom_out) {
+ err(1, "fopen: atom.xml");
+ }
+ writer->atom = atom_create(repo, writer->atom_out);
+ writer->tags_out = fs->fopen("tags.xml", "w");
+ if (!writer->tags_out) {
+ err(1, "fopen: tags.xml");
+ }
+ writer->tags = atom_create(repo, writer->tags_out);
+ writer->files = html_files_create(repo, fs);
return writer;
}
@@ -48,8 +61,16 @@ void html_repowriter_free(HtmlRepoWriter* writer) {
writer->log = NULL;
atom_free(writer->atom);
writer->atom = NULL;
+ if (writer->atom_out) {
+ writer->fs->fclose(writer->atom_out);
+ writer->atom_out = NULL;
+ }
atom_free(writer->tags);
writer->tags = NULL;
+ if (writer->tags_out) {
+ writer->fs->fclose(writer->tags_out);
+ writer->tags_out = NULL;
+ }
html_files_free(writer->files);
writer->files = NULL;
free(writer);
@@ -71,8 +92,8 @@ void html_repowriter_set_baseurl(HtmlRepoWriter* writer, const char* baseurl) {
}
void html_repowriter_begin(HtmlRepoWriter* writer) {
- mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
- mkdir("file", S_IRWXU | S_IRWXG | S_IRWXO);
+ writer->fs->mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
+ writer->fs->mkdir("file", S_IRWXU | S_IRWXG | S_IRWXO);
html_refs_begin(writer->refs);
html_log_begin(writer->log);
@@ -94,8 +115,8 @@ void html_repowriter_add_commit(HtmlRepoWriter* writer,
if (html_log_can_add_commits(writer->log)) {
html_log_add_commit(writer->log, git_commit);
- HtmlCommit* commit =
- html_commit_create(writer->repo, git_commit->oid, git_commit->summary);
+ HtmlCommit* commit = html_commit_create(
+ writer->repo, writer->fs, git_commit->oid, git_commit->summary);
html_commit_begin(commit);
html_commit_add_commit(commit, git_commit);
html_commit_end(commit);
@@ -122,7 +143,8 @@ void html_repowriter_add_reference(HtmlRepoWriter* writer,
void html_repowriter_add_file(HtmlRepoWriter* writer, const GitFile* file) {
html_files_add_file(writer->files, file);
- HtmlFileBlob* blob = html_fileblob_create(writer->repo, file->repo_path);
+ HtmlFileBlob* blob =
+ html_fileblob_create(writer->repo, writer->fs, file->repo_path);
html_fileblob_begin(blob);
html_fileblob_add_file(blob, file);
html_fileblob_end(blob);
diff --git a/src/writer/html/repo_writer.h b/src/writer/html/repo_writer.h
@@ -7,10 +7,12 @@
#include "git/file.h"
#include "git/reference.h"
#include "git/repo.h"
+#include "utils.h"
typedef struct HtmlRepoWriter HtmlRepoWriter;
-HtmlRepoWriter* html_repowriter_create(const GitRepo* repo);
+HtmlRepoWriter* html_repowriter_create(const GitRepo* repo,
+ const FileSystem* fs);
void html_repowriter_free(HtmlRepoWriter* writer);
void html_repowriter_set_log_cachefile(HtmlRepoWriter* writer,
const char* cachefile);
diff --git a/src/writer/html/repo_writer_tests.c b/src/writer/html/repo_writer_tests.c
@@ -0,0 +1,67 @@
+#include "writer/html/repo_writer.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "fs_inmemory.h"
+#include "git/commit.h"
+#include "git/repo.h"
+#include "utest.h"
+
+struct html_repo_writer {
+ int dummy;
+};
+
+UTEST_F_SETUP(html_repo_writer) {
+ inmemory_fs_clear();
+}
+
+UTEST_F_TEARDOWN(html_repo_writer) {}
+
+UTEST_F(html_repo_writer, orchestration) {
+ GitRepo repo = {
+ .name = "test-repo.git",
+ .short_name = "test-repo",
+ .description = "A test repository",
+ .owner = "Test Owner",
+ .clone_url = "git://example.com/test-repo.git",
+ .submodules = "",
+ .readme = "",
+ .license = "",
+ };
+ HtmlRepoWriter* writer = html_repowriter_create(&repo, g_fs_inmemory);
+ ASSERT_NE(NULL, writer);
+
+ GitCommit commit = {
+ .oid = "sha123",
+ .parentoid = "",
+ .summary = "Test commit",
+ .message = "Detailed message.",
+ .author_name = "User",
+ .author_email = "user@example.com",
+ };
+
+ html_repowriter_begin(writer);
+ html_repowriter_add_commit(writer, &commit);
+ html_repowriter_end(writer);
+ html_repowriter_free(writer);
+
+ /* Verify orchestration by checking multiple output files. */
+
+ /* 1. Log file exists and contains the commit message. */
+ const char* log_buf = inmemory_fs_get_buffer("log.html");
+ ASSERT_NE(NULL, log_buf);
+ EXPECT_NE(NULL, strstr(log_buf, "Test commit"));
+
+ /* 2. Atom feed exists and contains the commit entry. */
+ const char* atom_buf = inmemory_fs_get_buffer("atom.xml");
+ ASSERT_NE(NULL, atom_buf);
+ EXPECT_NE(NULL, strstr(atom_buf, "<title>Test commit</title>"));
+
+ /* 3. Individual commit page exists. */
+ const char* commit_buf = inmemory_fs_get_buffer("commit/sha123.html");
+ ASSERT_NE(NULL, commit_buf);
+ EXPECT_NE(NULL, strstr(commit_buf, "Test commit"));
+ EXPECT_NE(NULL, strstr(commit_buf, "sha123"));
+}
diff --git a/src/writer/index_writer.c b/src/writer/index_writer.c
@@ -9,7 +9,7 @@
typedef void (*IndexWriterVoidFunc)(void* impl);
typedef void (*IndexWriterStringFunc)(void* impl, const char* url);
-typedef void (*IndexWriterRepoInfoFunc)(void* impl, GitRepo* repo);
+typedef void (*IndexWriterRepoInfoFunc)(void* impl, const GitRepo* repo);
struct IndexWriter {
/* Writer implementation. */
@@ -23,18 +23,18 @@ struct IndexWriter {
IndexWriterVoidFunc end;
};
-static IndexWriter* htmlindexwriter_create(void);
+static IndexWriter* htmlindexwriter_create(FILE* out);
static void htmlindexwriter_free(IndexWriter* writer);
-static IndexWriter* gopherindexwriter_create(void);
+static IndexWriter* gopherindexwriter_create(FILE* out);
static void gopherindexwriter_free(IndexWriter* writer);
-IndexWriter* indexwriter_create(IndexWriterType type) {
+IndexWriter* indexwriter_create(IndexWriterType type, FILE* out) {
switch (type) {
case kIndexWriterTypeHtml:
- return htmlindexwriter_create();
+ return htmlindexwriter_create(out);
case kIndexWriterTypeGopher:
- return gopherindexwriter_create();
+ return gopherindexwriter_create(out);
}
errx(1, "unknown IndexWriterType %d", type);
}
@@ -64,7 +64,7 @@ void indexwriter_begin(IndexWriter* writer) {
writer->begin(writer->impl);
}
-void indexwriter_add_repo(IndexWriter* writer, GitRepo* repo) {
+void indexwriter_add_repo(IndexWriter* writer, const GitRepo* repo) {
writer->add_repo(writer->impl, repo);
}
@@ -74,9 +74,9 @@ void indexwriter_end(IndexWriter* writer) {
/* HtmlIndexWriter setup/teardown. */
-IndexWriter* htmlindexwriter_create(void) {
+static IndexWriter* htmlindexwriter_create(FILE* out) {
IndexWriter* writer = ecalloc(1, sizeof(IndexWriter));
- HtmlIndexWriter* html_writer = html_indexwriter_create();
+ HtmlIndexWriter* html_writer = html_indexwriter_create(out);
writer->type = kIndexWriterTypeHtml;
writer->impl = html_writer;
writer->set_me_url = (IndexWriterStringFunc)html_indexwriter_set_me_url;
@@ -86,7 +86,7 @@ IndexWriter* htmlindexwriter_create(void) {
return writer;
}
-void htmlindexwriter_free(IndexWriter* writer) {
+static void htmlindexwriter_free(IndexWriter* writer) {
if (!writer) {
return;
}
@@ -97,9 +97,9 @@ void htmlindexwriter_free(IndexWriter* writer) {
/* GopherIndexWriter setup/teardown. */
-IndexWriter* gopherindexwriter_create(void) {
+static IndexWriter* gopherindexwriter_create(FILE* out) {
IndexWriter* writer = ecalloc(1, sizeof(IndexWriter));
- GopherIndexWriter* gopher_writer = gopher_indexwriter_create();
+ GopherIndexWriter* gopher_writer = gopher_indexwriter_create(out);
writer->type = kIndexWriterTypeGopher;
writer->impl = gopher_writer;
writer->set_me_url = NULL;
@@ -109,7 +109,7 @@ IndexWriter* gopherindexwriter_create(void) {
return writer;
}
-void gopherindexwriter_free(IndexWriter* writer) {
+static void gopherindexwriter_free(IndexWriter* writer) {
if (!writer) {
return;
}
diff --git a/src/writer/index_writer.h b/src/writer/index_writer.h
@@ -2,19 +2,17 @@
#define GOUT_WRITER_INDEX_WRITER_H_
#include "git/repo.h"
+#include "gout_index.h"
-typedef enum {
- kIndexWriterTypeHtml,
- kIndexWriterTypeGopher,
-} IndexWriterType;
+#include <stdio.h>
typedef struct IndexWriter IndexWriter;
-IndexWriter* indexwriter_create(IndexWriterType type);
+IndexWriter* indexwriter_create(IndexWriterType type, FILE* out);
void indexwriter_free(IndexWriter* writer);
void indexwriter_set_me_url(IndexWriter* writer, const char* url);
void indexwriter_begin(IndexWriter* writer);
-void indexwriter_add_repo(IndexWriter* writer, GitRepo* repo);
+void indexwriter_add_repo(IndexWriter* writer, const GitRepo* repo);
void indexwriter_end(IndexWriter* writer);
#endif // GOUT_WRITER_INDEX_WRITER_H_
diff --git a/src/writer/index_writer_tests.c b/src/writer/index_writer_tests.c
@@ -0,0 +1,60 @@
+#include "writer/index_writer.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "git/repo.h"
+#include "gout_index.h"
+#include "test_utils.h"
+#include "utest.h"
+
+UTEST(index_writer, html) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+
+ IndexWriter* writer = indexwriter_create(kIndexWriterTypeHtml, out);
+ ASSERT_NE(NULL, writer);
+ indexwriter_set_me_url(writer, "https://me.example.com");
+
+ GitRepo repo = {.short_name = "test-repo",
+ .description = "test-desc",
+ .owner = "test-owner"};
+
+ indexwriter_begin(writer);
+ indexwriter_add_repo(writer, &repo);
+ indexwriter_end(writer);
+ indexwriter_free(writer);
+ fclose(out);
+
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "Repositories", "rel=\"me\"", "test-repo",
+ "test-desc", "test-owner");
+ free(buf);
+}
+
+UTEST(index_writer, gopher) {
+ char* buf = NULL;
+ size_t size = 0;
+ FILE* out = open_memstream(&buf, &size);
+
+ IndexWriter* writer = indexwriter_create(kIndexWriterTypeGopher, out);
+ ASSERT_NE(NULL, writer);
+ /* Should not crash even though Gopher doesn't support me_url */
+ indexwriter_set_me_url(writer, "https://me.example.com");
+
+ GitRepo repo = {.short_name = "test-repo",
+ .description = "test-desc",
+ .owner = "test-owner"};
+
+ indexwriter_begin(writer);
+ indexwriter_add_repo(writer, &repo);
+ indexwriter_end(writer);
+ indexwriter_free(writer);
+ fclose(out);
+
+ ASSERT_NE(NULL, buf);
+ EXPECT_STR_SEQUENCE(buf, "Repositories", "test-repo", "test-desc");
+ free(buf);
+}
diff --git a/src/writer/repo_writer.c b/src/writer/repo_writer.c
@@ -20,6 +20,7 @@ struct RepoWriter {
/* Writer implementation. */
RepoWriterType type;
void* impl;
+ const FileSystem* fs;
/* Writer configuration. */
RepoWriterSetLogCachefile set_log_cachefile;
@@ -34,18 +35,20 @@ struct RepoWriter {
RepoWriterEnd end;
};
-static RepoWriter* htmlrepowriter_create(GitRepo* repo);
+static RepoWriter* htmlrepowriter_create(GitRepo* repo, const FileSystem* fs);
static void htmlrepowriter_free(RepoWriter* writer);
-static RepoWriter* gopherrepowriter_create(GitRepo* repo);
+static RepoWriter* gopherrepowriter_create(GitRepo* repo, const FileSystem* fs);
static void gopherrepowriter_free(RepoWriter* writer);
-RepoWriter* repowriter_create(RepoWriterType type, GitRepo* repo) {
+RepoWriter* repowriter_create(RepoWriterType type,
+ GitRepo* repo,
+ const FileSystem* fs) {
switch (type) {
case kRepoWriterTypeHtml:
- return htmlrepowriter_create(repo);
+ return htmlrepowriter_create(repo, fs);
case kRepoWriterTypeGopher:
- return gopherrepowriter_create(repo);
+ return gopherrepowriter_create(repo, fs);
}
errx(1, "unknown RepoWriterType %d", type);
}
@@ -99,11 +102,12 @@ void repowriter_end(RepoWriter* writer) {
/* HtmlRepoWriter setup/teardown. */
-RepoWriter* htmlrepowriter_create(GitRepo* repo) {
+static RepoWriter* htmlrepowriter_create(GitRepo* repo, const FileSystem* fs) {
RepoWriter* writer = ecalloc(1, sizeof(RepoWriter));
- HtmlRepoWriter* html_writer = html_repowriter_create(repo);
+ HtmlRepoWriter* html_writer = html_repowriter_create(repo, fs);
writer->type = kRepoWriterTypeHtml;
writer->impl = html_writer;
+ writer->fs = fs;
writer->set_log_cachefile =
(RepoWriterSetLogCachefile)html_repowriter_set_log_cachefile;
writer->set_log_commit_limit =
@@ -117,7 +121,7 @@ RepoWriter* htmlrepowriter_create(GitRepo* repo) {
return writer;
}
-void htmlrepowriter_free(RepoWriter* writer) {
+static void htmlrepowriter_free(RepoWriter* writer) {
if (!writer) {
return;
}
@@ -128,11 +132,13 @@ void htmlrepowriter_free(RepoWriter* writer) {
/* GopherRepoWriter setup/teardown. */
-RepoWriter* gopherrepowriter_create(GitRepo* repo) {
+static RepoWriter* gopherrepowriter_create(GitRepo* repo,
+ const FileSystem* fs) {
RepoWriter* writer = ecalloc(1, sizeof(RepoWriter));
- GopherRepoWriter* gopher_writer = gopher_repowriter_create(repo);
+ GopherRepoWriter* gopher_writer = gopher_repowriter_create(repo, fs);
writer->type = kRepoWriterTypeGopher;
writer->impl = gopher_writer;
+ writer->fs = fs;
writer->set_log_cachefile =
(RepoWriterSetLogCachefile)gopher_repowriter_set_log_cachefile;
writer->set_log_commit_limit =
@@ -147,7 +153,7 @@ RepoWriter* gopherrepowriter_create(GitRepo* repo) {
return writer;
}
-void gopherrepowriter_free(RepoWriter* writer) {
+static void gopherrepowriter_free(RepoWriter* writer) {
if (!writer) {
return;
}
diff --git a/src/writer/repo_writer.h b/src/writer/repo_writer.h
@@ -7,15 +7,14 @@
#include "git/file.h"
#include "git/reference.h"
#include "git/repo.h"
-
-typedef enum {
- kRepoWriterTypeHtml,
- kRepoWriterTypeGopher,
-} RepoWriterType;
+#include "gout.h"
+#include "utils.h"
typedef struct RepoWriter RepoWriter;
-RepoWriter* repowriter_create(RepoWriterType type, GitRepo* repo);
+RepoWriter* repowriter_create(RepoWriterType type,
+ GitRepo* repo,
+ const FileSystem* fs);
void repowriter_free(RepoWriter* repo);
void repowriter_set_log_cachefile(RepoWriter* writer, const char* cachefile);