commit 5bc9a422994e6088c2baaf9c901c345acb11ce17
Author: Chris Bracken <chris@bracken.jp>
Date: Tue, 2 Jan 2024 22:01:12 +0900
Initial commit
Diffstat:
78 files changed, 4968 insertions(+), 0 deletions(-)
diff --git a/.clang-format b/.clang-format
@@ -0,0 +1,9 @@
+# Defines the Chromium style for automatic reformatting.
+# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
+BasedOnStyle: Chromium
+# This defaults to 'Auto'. Explicitly set it for a while, so that
+# 'vector<vector<int> >' in existing files gets formatted to
+# 'vector<vector<int>>'. ('Auto' means that clang-format will only use
+# 'int>>' if the file already contains at least one such instance.)
+Standard: Cpp11
+SortIncludes: true
diff --git a/.clang-tidy b/.clang-tidy
@@ -0,0 +1,43 @@
+# A YAML format of https://clang.llvm.org/extra/clang-tidy/.
+
+# Prefix check with "-" to ignore.
+Checks: >-
+ bugprone-argument-comment,
+ bugprone-use-after-move,
+ bugprone-unchecked-optional-access,
+ clang-analyzer-*,
+ clang-diagnostic-*,
+ darwin-*,
+ google-*,
+ modernize-use-default-member-init,
+ readability-identifier-naming,
+ -google-build-using-namespace,
+ -google-default-arguments,
+ -google-objc-function-naming,
+ -google-readability-casting,
+ -clang-analyzer-nullability.NullPassedToNonnull,
+ -clang-analyzer-nullability.NullablePassedToNonnull,
+ -clang-analyzer-nullability.NullReturnedFromNonnull,
+ -clang-analyzer-nullability.NullableReturnedFromNonnull,
+ performance-for-range-copy,
+ performance-inefficient-vector-operation,
+ performance-move-const-arg,
+ performance-move-constructor-init,
+ performance-unnecessary-copy-initialization,
+ performance-unnecessary-value-param
+
+CheckOptions:
+ - key: modernize-use-default-member-init.UseAssignment
+ value: true
+ - key: readability-identifier-naming.EnumConstantCase
+ value: "CamelCase"
+ - key: readability-identifier-naming.EnumConstantPrefix
+ value: "k"
+ - key: readability-identifier-naming.GlobalConstantCase
+ value: "CamelCase"
+ - key: readability-identifier-naming.GlobalConstantPrefix
+ value: "k"
+ - key: readability-identifier-naming.PrivateMemberCase
+ value: "lower_case"
+ - key: readability-identifier-naming.PrivateMemberSuffix
+ value: "_"
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,6 @@
+# Ignore symlink into out/debug for editor support.
+compile_commands.json
+.cache/
+
+# Ignore build output.
+out/
diff --git a/.gitmodules b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "third_party/googletest"]
+ path = third_party/googletest
+ url = https://github.com/google/googletest.git
diff --git a/.gn b/.gn
@@ -0,0 +1,24 @@
+# The location of the build configuration file.
+buildconfig = "//build/BUILDCONFIG.gn"
+
+# The secondary source root is a parallel directory tree where GN build files
+# are placed when they can not be placed directly in the source tree, e.g. for
+# third party source trees.
+secondary_source = "//secondary/"
+
+# Targets that should be included in the compile_commands.json output file.
+export_compile_commands = [
+ "//*",
+]
+
+# Edit as appropriate for the host system.
+default_args = {
+ system_include_dirs = [
+ "/usr/local/include",
+ "/Users/chris/.homebrew/include",
+ ]
+ system_lib_dirs = [
+ "/usr/local/lib",
+ "/Users/chris/.homebrew/lib",
+ ]
+}
diff --git a/BUILD.gn b/BUILD.gn
@@ -0,0 +1,115 @@
+# TODO(cbracken): remove personal include, lib dirs.
+config("gitout_config") {
+ include_dirs = [ "//" ] + system_include_dirs
+ lib_dirs = [] + system_lib_dirs
+ defines = [
+ "_XOPEN_SOURCE=700",
+ "_DEFAULT_SOURCE",
+ "_BSD_SOURCE",
+ ]
+ libs = [ "git2" ]
+ public_configs = [
+ "//build:compiler_std",
+ "//build:compiler_warnings",
+ "//build:strict_prototypes",
+ ]
+ if (is_debug) {
+ public_configs += [
+ "//build:debug",
+ "//build:no_optimize",
+ "//build:symbols",
+ ]
+ } else {
+ public_configs += [
+ "//build:release",
+ "//build:optimize_size",
+ "//build:lto",
+ ]
+ }
+}
+
+group("default") {
+ testonly = true
+ deps = [
+ ":gitout",
+ ":gitout_index",
+ ":gitout_tests",
+ ]
+}
+executable("gitout") {
+ sources = [ "gitout_main.c" ]
+ configs += [ ":gitout_config" ]
+ deps = [
+ ":format",
+ ":gitout_srcs",
+ ":utils",
+ ]
+}
+
+executable("gitout_index") {
+ sources = [ "gitout_index_main.c" ]
+ configs += [ ":gitout_config" ]
+ deps = [ ":gitout_index_srcs" ]
+}
+
+executable("gitout_tests") {
+ testonly = true
+
+ sources = [ "utils_test.cc" ]
+ deps = [
+ ":gitout_srcs",
+ "//third_party/googletest:gtest",
+ "//third_party/googletest:gtest_main",
+ ]
+}
+
+source_set("format") {
+ sources = [
+ "format.c",
+ "format.h",
+ ]
+ configs += [ ":gitout_config" ]
+}
+
+source_set("gitout_index_srcs") {
+ sources = [
+ "gitout_index.c",
+ "gitout_index.h",
+ ]
+ configs += [ ":gitout_config" ]
+ deps = [
+ ":security",
+ "//git",
+ "//writer:index_writer",
+ ]
+}
+
+source_set("gitout_srcs") {
+ sources = [
+ "gitout.c",
+ "gitout.h",
+ ]
+ configs += [ ":gitout_config" ]
+ deps = [
+ ":security",
+ "//git",
+ "//writer:repo_writer",
+ ]
+}
+
+source_set("security") {
+ sources = [
+ "security.c",
+ "security.h",
+ ]
+ configs += [ ":gitout_config" ]
+}
+
+source_set("utils") {
+ sources = [
+ "utils.c",
+ "utils.h",
+ ]
+ configs += [ ":gitout_config" ]
+ deps = [ "//third_party/openbsd" ]
+}
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,22 @@
+MIT/X Consortium License
+
+(c) 2015-2022 Hiltjo Posthuma <hiltjo@codemadness.org>
+(c) 2024 Chris Bracken <chris@bracken.jp>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
@@ -0,0 +1,24 @@
+.POSIX:
+
+NAME = gitout
+VERSION = 0.1
+
+all: debug release
+
+debug:
+ gn gen --args="is_debug=true" out/debug
+ ninja -C out/debug
+
+release:
+ gn gen --args="is_debug=false" out/release
+ ninja -C out/release
+
+test:
+ gn gen --args="is_debug=true" out/debug
+ ninja -C out/debug ":gitout_tests"
+ out/debug/gitout_tests
+
+clean:
+ rm -rf out
+
+.PHONY: all debug release clean
diff --git a/README.md b/README.md
@@ -0,0 +1,78 @@
+gitout
+======
+
+Gitout is a static git repository builder designed to be compatible with the
+excellent `stagit` tool, but adds support for multiple output formats: html,
+gopher, and gemini.
+
+Prerequisites
+-------------
+
+To build, the following tooling must be installed on the system:
+* [gn](https://gn.googlesource.com/gn)
+* [ninja](https://github.com/ninja-build/ninja)
+* A [clang](http://clang.llvm.org) toolchain
+
+To build and run, the following libraries must be installed on the system:
+* [libgit2](https://libgit2.org)
+
+
+Obtaining the source
+--------------------
+
+First, clone the repo. Then, initialise and fetch git submodules:
+
+ # Initialise local configuration file.
+ git submodule init
+
+ # Fetch data from the buildroot submodule.
+ git submodule update
+
+
+Updating the gn buildroot
+-------------------------
+
+To update the git submodules to a newer commit, simply run:
+
+ git submodule update --remote
+
+
+Building and running
+--------------------
+
+First, edit `.gn` as necessary to set the `system_include_dirs` and
+`system_lib_dirs` for your system.
+
+Next, generate the ninja build files under the `out` directory:
+
+ gn gen --args=is_debug=true out/debug
+ gn gen --args=is_debug=false out/release
+
+
+### Unit tests
+
+To build and run the unit tests, run:
+
+ gn gen --args=is_debug=true out/debug
+ ninja -C out/debug :gitout_tests
+ ./out/debug/gitout_tests
+
+or
+
+ make test
+
+
+### Executable release-mode binary
+
+To build and run the binary:
+
+ gn gen --args=is_debug=false out/release
+ ninja -C out/release :gitout :gitout_index
+ ./out/release/gitout
+ ./out/release/gitout_index
+
+or
+
+ make release
+ ./out/release/gitout
+ ./out/release/gitout_index
diff --git a/build/BUILD.gn b/build/BUILD.gn
@@ -0,0 +1,70 @@
+# Default language standards.
+config("compiler_std") {
+ cflags_c = [ "-std=c11" ]
+ cflags_cc = [ "-std=c++17" ]
+ cflags_objcc = [ "-std=c++17" ]
+}
+
+# Default compiler warnings.
+config("compiler_warnings") {
+ cflags = [
+ "-Wall",
+ "-Wextra",
+ "-Werror",
+ ]
+ cflags_cc = []
+ cflags_objcc = []
+}
+
+config("strict_prototypes") {
+ cflags = [
+ "-Wmissing-prototypes",
+ "-Wstrict-prototypes",
+ ]
+}
+
+# Debug mode build.
+config("debug") {
+ defines = [ "_DEBUG" ]
+}
+
+# Release mode build.
+config("release") {
+ defines = [ "NDEBUG" ]
+}
+
+# Disable optimisations.
+config("no_optimize") {
+ cflags = [ "-O0" ]
+}
+
+# Optimise for time performance.
+config("optimize") {
+ cflags = [ "-O2" ]
+}
+
+# Optimise for size.
+config("optimize_size") {
+ cflags = [ "-Os" ]
+}
+
+# Enable link-time-optimisation.
+config("lto") {
+ cflags = [ "-flto" ]
+ ldflags = [ "-flto" ]
+}
+
+# Regular build with symbols.
+config("symbols") {
+ cflags = [ "-g2" ]
+}
+
+# Minimal symbols, typically just enough for backtraces.
+config("min_symbols") {
+ cflags = [ "-g1" ]
+}
+
+# No symbols.
+config("no_symbols") {
+ cflags = [ "-g0" ]
+}
diff --git a/build/BUILDCONFIG.gn b/build/BUILDCONFIG.gn
@@ -0,0 +1,118 @@
+# =============================================================================
+# PLATFORM SELECTION
+# =============================================================================
+#
+# There are two main things to set: "os" and "cpu". The "toolchain" is the name
+# of the GN thing that encodes combinations of these things.
+#
+# Users typically only set the variables "target_os" and "target_cpu" in "gn
+# args", the rest are set up by our build and internal to GN.
+#
+# There are three different types of each of these things: The "host"
+# represents the computer doing the compile and never changes. The "target"
+# represents the main thing we're trying to build. The "current" represents
+# which configuration is currently being defined, which can be either the
+# host, the target, or something completely different (like nacl). GN will
+# run the same build file multiple times for the different required
+# configuration in the same build.
+#
+# This gives the following variables:
+# - host_os, host_cpu, host_toolchain
+# - target_os, target_cpu, default_toolchain
+# - current_os, current_cpu, current_toolchain.
+#
+# Note the default_toolchain isn't symmetrical (you would expect
+# target_toolchain). This is because the "default" toolchain is a GN built-in
+# concept, and "target" is something our build sets up that's symmetrical with
+# its GYP counterpart. Potentially the built-in default_toolchain variable
+# could be renamed in the future.
+#
+# When writing build files, to do something only for the host:
+# if (current_toolchain == host_toolchain) { ...
+
+if (target_os == "") {
+ target_os = host_os
+}
+if (target_cpu == "") {
+ target_cpu = host_cpu
+}
+if (current_cpu == "") {
+ current_cpu = target_cpu
+}
+if (current_os == "") {
+ current_os = target_os
+}
+
+# =============================================================================
+# PLATFORM SELECTION
+# =============================================================================
+#
+# There are two main things to set: "os" and "cpu". The "toolchain" is the name
+# of the GN thing that encodes combinations of these things.
+#
+# Users typically only set the variables "target_os" and "target_cpu" in "gn
+# args", the rest are set up by our build and internal to GN.
+#
+# There are three different types of each of these things: The "host"
+# represents the computer doing the compile and never changes. The "target"
+# represents the main thing we're trying to build. The "current" represents
+# which configuration is currently being defined, which can be either the
+# host, the target, or something completely different (like nacl). GN will
+# run the same build file multiple times for the different required
+# configuration in the same build.
+#
+# This gives the following variables:
+# - host_os, host_cpu, host_toolchain
+# - target_os, target_cpu, default_toolchain
+# - current_os, current_cpu, current_toolchain.
+#
+# Note the default_toolchain isn't symmetrical (you would expect
+# target_toolchain). This is because the "default" toolchain is a GN built-in
+# concept, and "target" is something our build sets up that's symmetrical with
+# its GYP counterpart. Potentially the built-in default_toolchain variable
+# could be renamed in the future.
+#
+# When writing build files, to do something only for the host:
+# if (current_toolchain == host_toolchain) { ...
+
+declare_args() {
+ is_debug = true
+ system_include_dirs = []
+ system_lib_dirs = []
+}
+
+# Options
+use_strip = !is_debug
+
+# All binary targets will get this list of configs by default.
+_shared_binary_target_configs = [
+ "//build:compiler_std",
+ "//build:compiler_warnings",
+]
+
+# Optimisations and debug/release mode.
+if (is_debug) {
+ _shared_binary_target_configs += [ "//build:debug" ]
+ _shared_binary_target_configs += [ "//build:no_optimize" ]
+ _shared_binary_target_configs += [ "//build:symbols" ]
+} else {
+ _shared_binary_target_configs += [ "//build:release" ]
+ _shared_binary_target_configs += [ "//build:optimize" ]
+ _shared_binary_target_configs += [ "//build:no_symbols" ]
+}
+
+# Apply that default list to the binary target types.
+set_defaults("executable") {
+ configs = _shared_binary_target_configs
+}
+set_defaults("static_library") {
+ configs = _shared_binary_target_configs
+}
+set_defaults("shared_library") {
+ configs = _shared_binary_target_configs
+}
+set_defaults("source_set") {
+ configs = _shared_binary_target_configs
+}
+
+set_default_toolchain("//build/toolchain:clang")
diff --git a/build/toolchain/BUILD.gn b/build/toolchain/BUILD.gn
@@ -0,0 +1,137 @@
+# Each toolchain must define "stamp" and "copy" tools,
+# but they are always the same in every toolchain.
+stamp_command = "touch {{output}}"
+stamp_description = "STAMP {{output}}"
+
+# We use link instead of copy; the way "copy" tool is being used is
+# compatible with links since Ninja is tracking changes to the source.
+copy_command = "ln -f {{source}} {{output}} 2>/dev/null || (rm -rf {{output}} && cp -af {{source}} {{output}})"
+copy_description = "COPY {{source}} {{output}}"
+
+toolchain("clang") {
+ cc = "clang"
+ cxx = "clang++"
+ ld = cxx
+ ar = "ar"
+
+ tool("cc") {
+ depfile = "{{output}}.d"
+ command = "$cc -MD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} -c {{source}} -o {{output}}"
+ depsformat = "gcc"
+ description = "CC {{output}}"
+ outputs =
+ [ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o" ]
+ }
+
+ tool("cxx") {
+ depfile = "{{output}}.d"
+ command = "$cxx -MD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_cc}} -c {{source}} -o {{output}}"
+ depsformat = "gcc"
+ description = "CXX {{output}}"
+ outputs =
+ [ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o" ]
+ }
+
+ tool("asm") {
+ depfile = "{{output}}.d"
+ command = "$cc -MD -MF $depfile {{defines}} {{include_dirs}} {{asmflags}} -c {{source}} -o {{output}}"
+ depsformat = "gcc"
+ description = "ASM {{output}}"
+ outputs =
+ [ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o" ]
+ }
+
+ tool("objc") {
+ depfile = "{{output}}.d"
+ command = "$cc -MD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} {{cflags_objc}} -c {{source}} -o {{output}}"
+ depsformat = "gcc"
+ description = "OBJC {{output}}"
+ outputs =
+ [ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o" ]
+ }
+
+ tool("objcxx") {
+ depfile = "{{output}}.d"
+ command = "$cxx -MD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} {{cflags_objcc}} -c {{source}} -o {{output}}"
+ depsformat = "gcc"
+ description = "OBJCXX {{output}}"
+ outputs =
+ [ "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o" ]
+ }
+
+ tool("alink") {
+ rspfile = "{{output}}.rsp"
+ command = "rm -f {{output}} && $ar rcs {{output}} {{inputs}}"
+ description = "AR {{output}}"
+ rspfile_content = "{{inputs}}"
+ outputs = [ "{{output_dir}}/{{target_output_name}}{{output_extension}}" ]
+ default_output_dir = "{{target_out_dir}}"
+ default_output_extension = ".a"
+ output_prefix = "lib"
+ }
+
+ tool("solink") {
+ outname = "{{target_output_name}}{{output_extension}}"
+ outfile = "{{output_dir}}/$outname"
+ rspfile = "$outfile.rsp"
+ unstripped_outfile = outfile
+ if (use_strip) {
+ unstripped_outfile = "{{output_dir}}/lib.unstripped/{{target_output_name}}{{output_extension}}"
+ }
+ if (target_os == "mac") {
+ command = "$ld -shared {{ldflags}} -Wl,-install_name,@rpath/\"{{target_output_name}}{{output_extension}}\" -o \"$unstripped_outfile\" -Wl,-filelist,\"$rspfile\" {{libs}} {{solibs}}"
+ rspfile_content = "{{inputs_newline}}"
+ default_output_extension = ".dylib"
+ } else {
+ command = "$ld -shared {{ldflags}} -o \"$unstripped_outfile\" -Wl,--Map=\"$unstripped_outfile.map\" -Wl,-soname=\"$outname\" @\"$rspfile\""
+ rspfile_content = "-Wl,--whole-archive {{inputs}} {{solibs}} -Wl,--no-whole-archive {{libs}}"
+ default_output_extension = ".so"
+ }
+ if (use_strip) {
+ command += " && strip --strip-all \"$unstripped_outfile\" \"$outfile\""
+ }
+ description = "SOLINK $outfile"
+ default_output_dir = "{{root_out_dir}}"
+ output_prefix = "lib"
+ outputs = [ outfile ]
+ if (outfile != unstripped_outfile) {
+ outputs += [ unstripped_outfile ]
+ }
+ }
+
+ tool("link") {
+ outfile = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
+ rspfile = "$outfile.rsp"
+ unstripped_outfile = outfile
+ if (use_strip) {
+ unstripped_outfile = "{{root_out_dir}}/exe.unstripped/{{target_output_name}}{{output_extension}}"
+ }
+ if (target_os == "mac") {
+ command = "$ld {{ldflags}} -o \"$unstripped_outfile\" -Wl,-filelist,\"$rspfile\" {{frameworks}} {{solibs}} {{libs}}"
+ rspfile_content = "{{inputs_newline}}"
+ } else {
+ command = "$ld {{ldflags}} -o \"$unstripped_outfile\" -Wl,--Map=\"$unstripped_outfile.map\" -Wl,--start-group @\"$rspfile\" {{solibs}} -Wl,--end-group {{libs}}"
+ rspfile_content = "{{inputs}}"
+ }
+ if (use_strip) {
+ command +=
+ " && cp \"$unstripped_outfile\" \"$outfile\" && strip \"$outfile\""
+ }
+ description = "LINK $outfile"
+ default_output_dir = "{{root_out_dir}}"
+ outputs = [ outfile ]
+ if (outfile != unstripped_outfile) {
+ outputs += [ unstripped_outfile ]
+ }
+ }
+
+ tool("stamp") {
+ command = stamp_command
+ description = stamp_description
+ }
+
+ tool("copy") {
+ command = copy_command
+ description = copy_description
+ }
+}
diff --git a/format.c b/format.c
@@ -0,0 +1,112 @@
+#include "format.h"
+
+#include <err.h>
+#include <string.h>
+#include <time.h>
+
+void print_time(FILE* out, time_t time, int timezone_offset) {
+ time_t local_time = time + (timezone_offset * 60);
+ struct tm* time_in = gmtime(&local_time);
+ if (!time_in) {
+ return;
+ }
+
+ char formatted_time[32];
+ if (!strftime(formatted_time, sizeof(formatted_time), "%a, %e %b %Y %H:%M:%S",
+ time_in)) {
+ err(1, "strftime");
+ }
+
+ char timezone_sign = timezone_offset < 0 ? '-' : '+';
+ int timezone_hours = (timezone_offset < 0 ? -1 : 1) * timezone_offset / 60;
+ int timezone_mins = (timezone_offset < 0 ? -1 : 1) * timezone_offset % 60;
+ char out_str[64];
+ if (snprintf(out_str, sizeof(out_str), "%s %c%02d%02d", formatted_time,
+ timezone_sign, timezone_hours, timezone_mins) < 0) {
+ err(1, "snprintf");
+ }
+ fprintf(out, "%s", out_str);
+}
+
+void print_time_z(FILE* out, time_t time) {
+ struct tm* time_in = gmtime(&time);
+ if (!time_in) {
+ return;
+ }
+
+ char formatted_time[32];
+ if (!strftime(formatted_time, sizeof(formatted_time), "%Y-%m-%dT%H:%M:%SZ",
+ time_in)) {
+ err(1, "strftime");
+ }
+ fprintf(out, "%s", formatted_time);
+}
+
+void print_time_short(FILE* out, time_t time) {
+ struct tm* time_in = gmtime(&time);
+ if (!time_in) {
+ return;
+ }
+
+ char formatted_time[32];
+ if (!strftime(formatted_time, sizeof(formatted_time), "%Y-%m-%d %H:%M",
+ time_in)) {
+ err(1, "strftime");
+ }
+ fprintf(out, "%s", formatted_time);
+}
+
+void print_percent_encoded(FILE* out, const char* str) {
+ static const char* hex_chars = "0123456789ABCDEF";
+
+ size_t str_len = strlen(str);
+ for (size_t i = 0; i < str_len; i++) {
+ unsigned char uc = str[i];
+ // NOTE: do not encode '/' for paths or ",-."
+ if (uc < ',' || uc >= 127 || (uc >= ':' && uc <= '@') || uc == '[' ||
+ uc == ']') {
+ fprintf(out, "%%%c%c", hex_chars[(uc >> 4) & 0x0f], hex_chars[uc & 0x0f]);
+ } else {
+ fprintf(out, "%c", uc);
+ }
+ }
+}
+
+void print_xml_encoded(FILE* out, const char* str) {
+ print_xml_encoded_len(out, str, -1, true);
+}
+
+void print_xml_encoded_len(FILE* out,
+ const char* str,
+ ssize_t str_len,
+ bool output_crlf) {
+ size_t len = (str_len >= 0) ? (size_t)str_len : strlen(str);
+ for (size_t i = 0; i < len && str[i] != '\0'; i++) {
+ char c = str[i];
+ switch (c) {
+ case '<':
+ fprintf(out, "<");
+ break;
+ case '>':
+ fprintf(out, ">");
+ break;
+ case '\'':
+ fprintf(out, "'");
+ break;
+ case '&':
+ fprintf(out, "&");
+ break;
+ case '"':
+ fprintf(out, """);
+ break;
+ case '\r':
+ case '\n':
+ if (output_crlf) {
+ fprintf(out, "%c", c);
+ }
+ break;
+ default:
+ fprintf(out, "%c", c);
+ }
+ }
+}
diff --git a/format.h b/format.h
@@ -0,0 +1,35 @@
+#ifndef GITOUT_FORMAT_H_
+#define GITOUT_FORMAT_H_
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <sys/types.h>
+
+/* Prints the formatted time in the local timezone: Wed 5 Dec 2023 23:59:59 +9
+ *
+ * time: time, in seconds since the epoch.
+ * tz_offset: timezone offset from UTC, in minutes.
+ */
+void print_time(FILE* out, time_t time, int tz_offset);
+
+/* Prints the formatted time in UTC: 2023-12-31T23:59:59Z */
+void print_time_z(FILE* out, time_t time);
+
+/* Prints the formatted time in UTC: 2023-12-31 23:59 */
+void print_time_short(FILE* out, time_t time);
+
+/* Prints a string to out, percent-encoded. See RFC3986 section 2.1. */
+void print_percent_encoded(FILE* out, const char* str);
+
+/* Prints a string to out, encoded HTML 2.0 / XML 1.0.
+ *
+ * If str_len >= 0, only the first str_len bytes of str are read.
+ * If output_crlf is true, also prints '\r' and '\n' characters.
+ */
+void print_xml_encoded(FILE* out, const char* str);
+void print_xml_encoded_len(FILE* out,
+ const char* str,
+ ssize_t str_len,
+ bool output_crlf);
+
+#endif // GITOUT_FORMAT_H_
diff --git a/git/BUILD.gn b/git/BUILD.gn
@@ -0,0 +1,28 @@
+source_set("internal") {
+ sources = [ "internal.h" ]
+ configs += [ "//:gitout_config" ]
+ visibility = [ ":*" ]
+}
+
+source_set("git") {
+ sources = [
+ "commit.c",
+ "commit.h",
+ "delta.c",
+ "delta.h",
+ "file.c",
+ "file.h",
+ "git.c",
+ "git.h",
+ "reference.c",
+ "reference.h",
+ "repo.c",
+ "repo.h",
+ ]
+ configs += [ "//:gitout_config" ]
+ deps = [
+ ":internal",
+ "//:utils",
+ "//third_party/openbsd",
+ ]
+}
diff --git a/git/commit.c b/git/commit.c
@@ -0,0 +1,130 @@
+#include "git/commit.h"
+
+#include <err.h>
+#include <git2/commit.h>
+#include <git2/diff.h>
+#include <git2/oid.h>
+#include <git2/patch.h>
+#include <git2/tree.h>
+#include <git2/types.h>
+#include <stdlib.h>
+
+#include "git/internal.h"
+#include "utils.h"
+
+CommitInfo* commitinfo_create(const git_oid* oid, git_repository* repo) {
+ CommitInfo* ci = ecalloc(1, sizeof(CommitInfo));
+ if (git_commit_lookup((git_commit**)&ci->commit_, repo, oid)) {
+ errx(1, "git_commit_lookup");
+ }
+
+ // Get OID, parent OID.
+ ci->oid = ecalloc(GIT_OID_SHA1_HEXSIZE + 1, sizeof(char));
+ git_oid_tostr(ci->oid, GIT_OID_SHA1_HEXSIZE + 1, git_commit_id(ci->commit_));
+ ci->parentoid = ecalloc(GIT_OID_SHA1_HEXSIZE + 1, sizeof(char));
+ git_oid_tostr(ci->parentoid, GIT_OID_SHA1_HEXSIZE + 1,
+ git_commit_parent_id(ci->commit_, 0));
+
+ // Set commit summary, message.
+ ci->summary = git_commit_summary(ci->commit_);
+ ci->msg = git_commit_message(ci->commit_);
+
+ // Get commit time, tz offset.
+ ci->commit_time = git_commit_time(ci->commit_);
+ ci->commit_timezone_offset = git_commit_time_offset(ci->commit_);
+
+ // Get author info.
+ const git_signature* author = git_commit_author(ci->commit_);
+ ci->author_name = author->name;
+ ci->author_email = author->email;
+ ci->author_time = author->when.time;
+ ci->author_timezone_offset = author->when.offset;
+
+ // Look up commit tree.
+ git_tree* commit_tree = NULL;
+ if (git_tree_lookup(&commit_tree, repo, git_commit_tree_id(ci->commit_))) {
+ errx(1, "git_tree_lookup");
+ }
+
+ // Look up parent tree, if there is a parent commit.
+ git_commit* parent = NULL;
+ git_tree* parent_tree = NULL;
+ if (!git_commit_parent(&parent, ci->commit_, 0)) {
+ if (git_tree_lookup(&parent_tree, repo, git_commit_tree_id(parent))) {
+ errx(1, "git_tree_lookup");
+ }
+ git_commit_free(parent);
+ parent = NULL;
+ }
+
+ // Compute the diff.
+ git_diff* diff = NULL;
+ git_diff_options opts;
+ git_diff_options_init(&opts, GIT_DIFF_OPTIONS_VERSION);
+ opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH | GIT_DIFF_IGNORE_SUBMODULES |
+ GIT_DIFF_INCLUDE_TYPECHANGE;
+ if (git_diff_tree_to_tree(&diff, repo, parent_tree, commit_tree, &opts)) {
+ errx(1, "git_diff_tree_to_tree");
+ }
+ git_tree_free(commit_tree);
+ git_tree_free(parent_tree);
+
+ git_diff_free(ci->diff_);
+ ci->diff_ = diff;
+
+ git_diff_find_options fopts;
+ if (git_diff_find_options_init(&fopts, GIT_DIFF_FIND_OPTIONS_VERSION)) {
+ errx(1, "git_diff_find_init_options");
+ }
+
+ /* find renames and copies, exact matches (no heuristic) for renames. */
+ fopts.flags |= GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES |
+ GIT_DIFF_FIND_EXACT_MATCH_ONLY;
+ if (git_diff_find_similar(ci->diff_, &fopts)) {
+ errx(1, "git_diff_find_similar");
+ }
+
+ // Add deltas.
+ ci->addcount = 0;
+ ci->delcount = 0;
+ size_t deltas_len = git_diff_num_deltas(ci->diff_);
+ ci->deltas = ecalloc(deltas_len, sizeof(DeltaInfo*));
+ size_t deltas_out_count = 0;
+ for (size_t i = 0; i < deltas_len; i++) {
+ git_patch* patch = NULL;
+ if (git_patch_from_diff(&patch, ci->diff_, i)) {
+ continue;
+ }
+
+ // Hand ownership of patch to di.
+ DeltaInfo* di = deltainfo_create(patch);
+ if (di) {
+ ci->deltas[deltas_out_count++] = di;
+ ci->addcount += di->addcount;
+ ci->delcount += di->delcount;
+ }
+ }
+ ci->deltas_len = deltas_out_count;
+ ci->filecount = deltas_len;
+ return ci;
+}
+
+void commitinfo_free(CommitInfo* ci) {
+ if (!ci) {
+ return;
+ }
+ free(ci->oid);
+ free(ci->parentoid);
+ for (size_t i = 0; i < ci->deltas_len; i++) {
+ deltainfo_free(ci->deltas[i]);
+ ci->deltas[i] = NULL;
+ }
+ free(ci->deltas);
+ ci->deltas = NULL;
+
+ git_diff_free(ci->diff_);
+ ci->diff_ = NULL;
+ git_commit_free(ci->commit_);
+ ci->commit_ = NULL;
+ free(ci);
+}
diff --git a/git/commit.h b/git/commit.h
@@ -0,0 +1,30 @@
+#ifndef GITOUT_GIT_COMMIT_H_
+#define GITOUT_GIT_COMMIT_H_
+
+#include <time.h>
+
+#include "git/delta.h"
+
+typedef struct {
+ char* oid;
+ char* parentoid;
+ const char* summary;
+ const char* msg;
+ time_t commit_time;
+ int commit_timezone_offset;
+ const char* author_name;
+ const char* author_email;
+ time_t author_time;
+ int author_timezone_offset;
+ DeltaInfo** deltas;
+ size_t deltas_len;
+ size_t addcount;
+ size_t delcount;
+ size_t filecount;
+
+ /* private */
+ void* commit_;
+ void* diff_;
+} CommitInfo;
+
+#endif // GITOUT_GIT_COMMIT_H_
diff --git a/git/delta.c b/git/delta.c
@@ -0,0 +1,190 @@
+#include "git/delta.h"
+
+#include <git2/diff.h>
+#include <git2/patch.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "git/internal.h"
+#include "utils.h"
+
+/* HunkLines */
+static HunkLine* hunkline_create(git_patch* patch,
+ size_t hunk_id,
+ size_t line_id);
+static void hunkline_free(HunkLine* hunk_line);
+
+/* Hunks */
+static Hunk* hunk_create(git_patch* patch, size_t id);
+static void hunk_free(Hunk* hunk);
+
+/* Deltas */
+static char status_for_delta(const git_diff_delta* delta);
+static char* deltainfo_graph(const DeltaInfo* di,
+ size_t length,
+ char character,
+ size_t max_width);
+
+HunkLine* hunkline_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;
+ }
+
+ HunkLine* line_out = ecalloc(1, sizeof(HunkLine));
+ line_out->id = line_id;
+ line_out->old_lineno = line->old_lineno;
+ line_out->new_lineno = line->new_lineno;
+ line_out->content = line->content;
+ line_out->content_len = line->content_len;
+ return line_out;
+}
+
+void hunkline_free(HunkLine* hunk_line) {
+ free(hunk_line);
+}
+
+Hunk* hunk_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)) {
+ return NULL;
+ }
+
+ Hunk* hunk_out = ecalloc(1, sizeof(Hunk));
+ hunk_out->id = hunk_id;
+ hunk_out->header = hunk->header;
+ hunk_out->lines = ecalloc(line_count, sizeof(HunkLine*));
+ size_t lines_out_count = 0;
+ for (size_t i = 0; i < line_count; i++) {
+ HunkLine* hunk_line = hunkline_create(patch, hunk_id, i);
+ if (hunk_line) {
+ hunk_out->lines[lines_out_count++] = hunk_line;
+ }
+ }
+ hunk_out->lines_len = lines_out_count;
+ return hunk_out;
+}
+
+void hunk_free(Hunk* hunk) {
+ if (!hunk) {
+ return;
+ }
+ for (size_t i = 0; i < hunk->lines_len; i++) {
+ hunkline_free(hunk->lines[i]);
+ hunk->lines[i] = NULL;
+ }
+ free(hunk->lines);
+ hunk->lines = NULL;
+ free(hunk);
+}
+
+char status_for_delta(const git_diff_delta* delta) {
+ switch (delta->status) {
+ case GIT_DELTA_ADDED:
+ return 'A';
+ case GIT_DELTA_COPIED:
+ return 'C';
+ case GIT_DELTA_DELETED:
+ return 'D';
+ case GIT_DELTA_MODIFIED:
+ return 'M';
+ case GIT_DELTA_RENAMED:
+ return 'R';
+ case GIT_DELTA_TYPECHANGE:
+ return 'T';
+ case GIT_DELTA_CONFLICTED:
+ case GIT_DELTA_IGNORED:
+ case GIT_DELTA_UNMODIFIED:
+ case GIT_DELTA_UNREADABLE:
+ case GIT_DELTA_UNTRACKED:
+ return ' ';
+ }
+ return ' ';
+}
+
+DeltaInfo* deltainfo_create(git_patch* patch) {
+ DeltaInfo* di = ecalloc(1, sizeof(DeltaInfo));
+
+ /* Take ownership of patch. */
+ di->patch_ = patch;
+
+ const git_diff_delta* delta = git_patch_get_delta(patch);
+ di->status = status_for_delta(delta);
+ di->is_binary = delta->flags & GIT_DIFF_FLAG_BINARY;
+ di->old_file_path = delta->old_file.path;
+ di->new_file_path = delta->new_file.path;
+ di->addcount = 0;
+ di->delcount = 0;
+
+ /* Skip stats for binary data. */
+ if (di->is_binary) {
+ return di;
+ }
+
+ /* Populate hunks array. */
+ size_t hunk_count = git_patch_num_hunks(patch);
+ di->hunks = ecalloc(hunk_count, sizeof(Hunk*));
+ size_t hunks_out_count = 0;
+ for (size_t i = 0; i < hunk_count; i++) {
+ const git_diff_hunk* hunk;
+ size_t line_count;
+ if (!git_patch_get_hunk(&hunk, &line_count, patch, i)) {
+ Hunk* hunk = hunk_create(patch, i);
+ if (hunk) {
+ di->hunks[hunks_out_count++] = hunk;
+ }
+ }
+ }
+ di->hunks_len = hunks_out_count;
+
+ /* Increment added/deleted line counts. */
+ for (size_t i = 0; i < di->hunks_len; i++) {
+ Hunk* hunk = di->hunks[i];
+ for (size_t j = 0; j < hunk->lines_len; j++) {
+ HunkLine* line = hunk->lines[j];
+ if (line->old_lineno == -1) {
+ di->addcount++;
+ } else if (line->new_lineno == -1) {
+ di->delcount++;
+ }
+ }
+ }
+ return di;
+}
+
+void deltainfo_free(DeltaInfo* di) {
+ if (!di) {
+ return;
+ }
+ for (size_t i = 0; i < di->hunks_len; i++) {
+ hunk_free(di->hunks[i]);
+ di->hunks[i] = NULL;
+ }
+ free(di->hunks);
+ di->hunks = NULL;
+ git_patch_free(di->patch_);
+ di->patch_ = NULL;
+ free(di);
+}
+
+char* deltainfo_graph(const DeltaInfo* di,
+ size_t length,
+ char c,
+ size_t max_width) {
+ size_t changed = di->addcount + di->delcount;
+ if (changed > max_width && length > 0) {
+ length = ((float)max_width / changed * length) + 1;
+ }
+ char* graph = ecalloc(length + 1, sizeof(char));
+ memset(graph, c, length);
+ return graph;
+}
+
+char* deltainfo_added_graph(const DeltaInfo* di, size_t max_width) {
+ return deltainfo_graph(di, di->addcount, '+', max_width);
+}
+
+char* deltainfo_deleted_graph(const DeltaInfo* di, size_t max_width) {
+ return deltainfo_graph(di, di->delcount, '-', max_width);
+}
diff --git a/git/delta.h b/git/delta.h
@@ -0,0 +1,39 @@
+#ifndef GITOUT_GIT_DELTA_H_
+#define GITOUT_GIT_DELTA_H_
+
+#include <stdbool.h>
+#include <stddef.h>
+
+typedef struct {
+ size_t id;
+ int old_lineno;
+ int new_lineno;
+ const char* content;
+ size_t content_len;
+} HunkLine;
+
+typedef struct {
+ size_t id;
+ const char* header;
+ HunkLine** lines;
+ size_t lines_len;
+} Hunk;
+
+typedef struct {
+ bool is_binary;
+ char status;
+ const char* old_file_path;
+ const char* new_file_path;
+ size_t addcount;
+ size_t delcount;
+ Hunk** hunks;
+ size_t hunks_len;
+
+ /* private */
+ void* patch_;
+} DeltaInfo;
+
+char* deltainfo_added_graph(const DeltaInfo* di, size_t max_width);
+char* deltainfo_deleted_graph(const DeltaInfo* di, size_t max_width);
+
+#endif // GITOUT_GIT_DELTA_H_
diff --git a/git/file.c b/git/file.c
@@ -0,0 +1,30 @@
+#include "git/file.h"
+
+#include <stdlib.h>
+
+#include "git/internal.h"
+#include "utils.h"
+
+FileInfo* fileinfo_create(FileType type,
+ const char* mode,
+ const char* display_path,
+ const char* repo_path,
+ const char* commit_oid,
+ ssize_t size_bytes,
+ ssize_t size_lines,
+ const char* content) {
+ FileInfo* fi = ecalloc(1, sizeof(FileInfo));
+ fi->type = type;
+ fi->mode = mode;
+ fi->display_path = display_path;
+ fi->repo_path = repo_path;
+ fi->commit_oid = commit_oid;
+ fi->size_bytes = size_bytes;
+ fi->size_lines = size_lines;
+ fi->content = content;
+ return fi;
+}
+
+void fileinfo_free(FileInfo* fi) {
+ free(fi);
+}
diff --git a/git/file.h b/git/file.h
@@ -0,0 +1,23 @@
+#ifndef GITOUT_GIT_FILE_H_
+#define GITOUT_GIT_FILE_H_
+
+#include <sys/types.h>
+
+typedef enum {
+ kFileTypeFile,
+ kFileTypeSubmodule,
+} FileType;
+
+typedef struct {
+ FileType type;
+ const char* mode;
+ const char* display_path;
+ const char* repo_path;
+ // Submodule commit OID. Empty string for files.
+ const char* commit_oid;
+ ssize_t size_bytes;
+ ssize_t size_lines;
+ const char* content;
+} FileInfo;
+
+#endif // GITOUT_GIT_FILE_H_
diff --git a/git/git.c b/git/git.c
@@ -0,0 +1,24 @@
+#include "git.h"
+
+#include <git2/common.h>
+#include <git2/config.h>
+#include <git2/global.h>
+#include <git2/oid.h>
+
+/* Global const data. */
+const size_t kOidLen = GIT_OID_SHA1_HEXSIZE + 1;
+
+void gitout_git_initialize(void) {
+ /* do not search outside the git repository:
+ GIT_CONFIG_LEVEL_APP is the highest level currently */
+ git_libgit2_init();
+ for (int i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) {
+ git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
+ }
+ /* do not require the git repository to be owned by the current user */
+ git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
+}
+
+void gitout_git_shutdown(void) {
+ git_libgit2_shutdown();
+}
diff --git a/git/git.h b/git/git.h
@@ -0,0 +1,11 @@
+#ifndef GITOUT_GIT_GIT_H_
+#define GITOUT_GIT_GIT_H_
+
+#include <stddef.h>
+
+extern const size_t kOidLen;
+
+void gitout_git_initialize(void);
+void gitout_git_shutdown(void);
+
+#endif // GITOUT_GIT_GIT_H_
diff --git a/git/internal.h b/git/internal.h
@@ -0,0 +1,31 @@
+#ifndef GITOUT_GIT_INTERNAL_H_
+#define GITOUT_GIT_INTERNAL_H_
+
+#include <git2.h>
+
+#include "git/commit.h"
+#include "git/delta.h"
+#include "git/file.h"
+#include "git/reference.h"
+
+CommitInfo* commitinfo_create(const git_oid* oid, git_repository* repo);
+void commitinfo_free(CommitInfo* ci);
+
+DeltaInfo* deltainfo_create(git_patch* patch);
+void deltainfo_free(DeltaInfo* di);
+
+FileInfo* fileinfo_create(FileType type,
+ const char* mode,
+ const char* display_path,
+ const char* repo_path,
+ const char* commit_oid,
+ ssize_t size_bytes,
+ ssize_t size_lines,
+ const char* content);
+void fileinfo_free(FileInfo* fi);
+
+ReferenceInfo* referenceinfo_create(git_repository* repo, git_reference* ref);
+void referenceinfo_free(ReferenceInfo* ri);
+int referenceinfo_compare(const void* r1, const void* r2);
+
+#endif // GITOUT_GIT_INTERNAL_H_
diff --git a/git/reference.c b/git/reference.c
@@ -0,0 +1,83 @@
+#include "git/reference.h"
+
+#include <err.h>
+#include <git2/object.h>
+#include <git2/oid.h>
+#include <git2/refs.h>
+#include <git2/types.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "git/internal.h"
+#include "utils.h"
+
+ReferenceInfo* referenceinfo_create(git_repository* repo, git_reference* ref) {
+ ReferenceInfo* ri = ecalloc(1, sizeof(ReferenceInfo));
+
+ // Set ref.
+ if (git_reference_type(ref) == GIT_REFERENCE_SYMBOLIC) {
+ git_reference* direct_ref = NULL;
+ git_reference_resolve(&direct_ref, ref);
+ git_reference_free(ref);
+ ref = direct_ref;
+ }
+ if (!git_reference_target(ref)) {
+ errx(1, "git_reference_target");
+ }
+ ri->ref_ = ref;
+
+ // Set type.
+ if (git_reference_is_branch(ri->ref_)) {
+ ri->type = kReftypeBranch;
+ } else if (git_reference_is_tag(ri->ref_)) {
+ ri->type = kReftypeTag;
+ } else {
+ errx(1, "not a branch or tag");
+ }
+
+ // Set shorthand.
+ ri->shorthand = git_reference_shorthand(ri->ref_);
+
+ // Create a CommitInfo from the object.
+ git_object* obj = NULL;
+ git_reference_peel(&obj, ref, GIT_OBJECT_ANY);
+ const git_oid* id = git_object_id(obj);
+ git_object_free(obj);
+ if (!id) {
+ errx(1, "git_object_id");
+ }
+ ri->ci = commitinfo_create(id, repo);
+ return ri;
+}
+
+void referenceinfo_free(ReferenceInfo* ri) {
+ if (!ri) {
+ return;
+ }
+ git_reference_free(ri->ref_);
+ ri->ref_ = NULL;
+ commitinfo_free(ri->ci);
+ ri->ci = NULL;
+ free(ri);
+}
+
+int referenceinfo_compare(const void* a, const void* b) {
+ ReferenceInfo* r1 = *(ReferenceInfo**)a;
+ ReferenceInfo* r2 = *(ReferenceInfo**)b;
+ int r = git_reference_is_tag(r1->ref_) - git_reference_is_tag(r2->ref_);
+ if (r != 0) {
+ return r;
+ }
+
+ time_t t1 = r1->ci->author_time;
+ time_t t2 = r2->ci->author_time;
+ if (t1 > t2) {
+ return -1;
+ }
+ if (t1 < t2) {
+ return 1;
+ }
+ return strcmp(git_reference_shorthand(r1->ref_),
+ git_reference_shorthand(r2->ref_));
+}
diff --git a/git/reference.h b/git/reference.h
@@ -0,0 +1,21 @@
+#ifndef GITOUT_GIT_REFERENCE_H_
+#define GITOUT_GIT_REFERENCE_H_
+
+#include "git/commit.h"
+
+typedef enum {
+ kReftypeBranch,
+ kReftypeTag,
+} RefType;
+
+/* reference and associated data for sorting */
+typedef struct {
+ RefType type;
+ const char* shorthand;
+ CommitInfo* ci;
+
+ /* private */
+ void* ref_;
+} ReferenceInfo;
+
+#endif // GITOUT_GIT_REFERENCE_H_
diff --git a/git/repo.c b/git/repo.c
@@ -0,0 +1,448 @@
+#include "git/repo.h"
+
+#include <err.h>
+#include <git2/blob.h>
+#include <git2/commit.h>
+#include <git2/object.h>
+#include <git2/oid.h>
+#include <git2/refs.h>
+#include <git2/repository.h>
+#include <git2/revparse.h>
+#include <git2/revwalk.h>
+#include <git2/tree.h>
+#include <git2/types.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/unistd.h>
+#include <unistd.h>
+
+#include "git/internal.h"
+#include "third_party/openbsd/reallocarray.h"
+#include "utils.h"
+
+/* Local const data */
+static const char* kLicenses[] = {"HEAD:LICENSE", "HEAD:LICENSE.md",
+ "HEAD:COPYING"};
+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*);
+
+/* 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* format_filemode(git_filemode_t m);
+
+/* RepoInfo utilities. */
+static char* repoinfo_name_from_path(const char* repo_path);
+static char* repoinfo_shortname_from_name(const char* name);
+static bool repoinfo_has_blob(git_repository* repo, const char* file);
+static const char* repoinfo_first_matching_file(git_repository* repo,
+ const char** files,
+ size_t files_len);
+static bool repoinfo_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;
+}
+
+bool string_ends_with(const char* str, const char* suffix) {
+ if (!str || !suffix) {
+ return false;
+ }
+ size_t str_len = strlen(str);
+ size_t suffix_len = strlen(suffix);
+ if (str_len < suffix_len) {
+ return false;
+ }
+ return strncmp(str + str_len - suffix_len, suffix, suffix_len) == 0;
+}
+
+char* first_line_of_file(const char* path) {
+ FILE* f = fopen(path, "r");
+ if (!f) {
+ return estrdup("");
+ }
+ char* buf = NULL;
+ size_t buf_size = 0;
+ size_t len = getline(&buf, &buf_size, f);
+ fclose(f);
+ if (!buf) {
+ return estrdup("");
+ }
+ // Remove trailing newline.
+ if (len > 0) {
+ buf[len - 1] = '\0';
+ }
+ 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* format_filemode(git_filemode_t m) {
+ char* mode = ecalloc(11, sizeof(char));
+ memset(mode, '-', 10);
+ mode[10] = '\0';
+
+ if (S_ISREG(m)) {
+ mode[0] = '-';
+ } else if (S_ISBLK(m)) {
+ mode[0] = 'b';
+ } else if (S_ISCHR(m)) {
+ mode[0] = 'c';
+ } else if (S_ISDIR(m)) {
+ mode[0] = 'd';
+ } else if (S_ISFIFO(m)) {
+ mode[0] = 'p';
+ } else if (S_ISLNK(m)) {
+ mode[0] = 'l';
+ } else if (S_ISSOCK(m)) {
+ mode[0] = 's';
+ } else {
+ mode[0] = '?';
+ }
+
+ 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* repoinfo_name_from_path(const char* repo_path) {
+ char path[PATH_MAX];
+ estrlcpy(path, repo_path, sizeof(path));
+ const char* filename = basename(path);
+ if (!filename) {
+ err(1, "basename");
+ }
+ return estrdup(filename);
+}
+
+char* repoinfo_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);
+ size_t suffix_len = strlen(".git");
+ short_name[short_name_len - suffix_len] = '\0';
+ }
+ return short_name;
+}
+
+bool repoinfo_has_blob(git_repository* repo, const char* file) {
+ git_object* obj = NULL;
+ if (git_revparse_single(&obj, repo, file)) {
+ return false;
+ }
+ bool has_blob = git_object_type(obj) == GIT_OBJECT_BLOB;
+ git_object_free(obj);
+ return has_blob;
+}
+
+const char* repoinfo_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 (repoinfo_has_blob(repo, filename)) {
+ return filename + strlen("HEAD:");
+ }
+ }
+ return "";
+}
+
+RepoInfo* repoinfo_create(const char* path) {
+ RepoInfo* repo_info = ecalloc(1, sizeof(RepoInfo));
+ git_repository* repo = NULL;
+ git_repository_open_flag_t kRepoOpenFlags = GIT_REPOSITORY_OPEN_NO_SEARCH;
+ if (git_repository_open_ext(&repo, path, kRepoOpenFlags, NULL)) {
+ errx(1, "git_repository_open_ext");
+ }
+ repo_info->repo_ = repo;
+
+ char repo_path[PATH_MAX];
+ if (!realpath(path, repo_path)) {
+ err(1, "realpath");
+ }
+ char git_path[PATH_MAX];
+ path_concat(git_path, sizeof(git_path), repo_path, ".git");
+ if (access(git_path, F_OK) != 0) {
+ estrlcpy(git_path, repo_path, sizeof(git_path));
+ }
+ char owner_path[PATH_MAX];
+ path_concat(owner_path, sizeof(owner_path), git_path, "owner");
+ char desc_path[PATH_MAX];
+ path_concat(desc_path, sizeof(desc_path), git_path, "description");
+ char url_path[PATH_MAX];
+ path_concat(url_path, sizeof(url_path), git_path, "url");
+
+ repo_info->name = repoinfo_name_from_path(repo_path);
+ repo_info->short_name = repoinfo_shortname_from_name(repo_info->name);
+ repo_info->owner = first_line_of_file(owner_path);
+ repo_info->description = first_line_of_file(desc_path);
+ repo_info->clone_url = first_line_of_file(url_path);
+ repo_info->submodules =
+ repoinfo_has_blob(repo_info->repo_, "HEAD:.gitmodules") ? ".gitmodules"
+ : "";
+ repo_info->readme =
+ repoinfo_first_matching_file(repo_info->repo_, kReadmes, kReadmesLen);
+ repo_info->license =
+ repoinfo_first_matching_file(repo_info->repo_, kLicenses, kLicensesLen);
+ return repo_info;
+}
+
+void repoinfo_free(RepoInfo* repo_info) {
+ if (!repo_info) {
+ return;
+ }
+ free(repo_info->name);
+ repo_info->name = NULL;
+ free(repo_info->short_name);
+ repo_info->short_name = NULL;
+ free(repo_info->owner);
+ repo_info->owner = NULL;
+ free(repo_info->description);
+ repo_info->description = NULL;
+ free(repo_info->clone_url);
+ repo_info->clone_url = NULL;
+ git_repository_free(repo_info->repo_);
+ repo_info->repo_ = NULL;
+ free(repo_info);
+}
+
+void repoinfo_for_commit(RepoInfo* repo_info,
+ const char* spec,
+ CommitCallback cb,
+ void* user_data) {
+ const git_oid* start_oid = oid_for_spec(repo_info->repo_, spec);
+ if (start_oid == NULL) {
+ return;
+ }
+
+ git_revwalk* revwalk = NULL;
+ if (git_revwalk_new(&revwalk, repo_info->repo_) != 0) {
+ errx(1, "git_revwalk_new");
+ }
+ git_revwalk_push(revwalk, start_oid);
+
+ git_oid current;
+ if (!git_revwalk_next(¤t, revwalk)) {
+ CommitInfo* ci = commitinfo_create(¤t, repo_info->repo_);
+ cb(ci, user_data);
+ commitinfo_free(ci);
+ }
+ git_revwalk_free(revwalk);
+}
+
+void repoinfo_for_each_commit(RepoInfo* repo_info,
+ CommitCallback cb,
+ void* user_data) {
+ const git_oid* start_oid = oid_for_spec(repo_info->repo_, "HEAD");
+
+ git_revwalk* revwalk = NULL;
+ if (git_revwalk_new(&revwalk, repo_info->repo_) != 0) {
+ errx(1, "git_revwalk_new");
+ }
+ git_revwalk_push(revwalk, start_oid);
+
+ git_oid current;
+ while (!git_revwalk_next(¤t, revwalk)) {
+ CommitInfo* ci = commitinfo_create(¤t, repo_info->repo_);
+ cb(ci, user_data);
+ commitinfo_free(ci);
+ }
+ git_revwalk_free(revwalk);
+}
+
+void repoinfo_for_each_reference(RepoInfo* repo_info,
+ ReferenceCallback cb,
+ void* user_data) {
+ git_reference_iterator* it = NULL;
+ if (git_reference_iterator_new(&it, repo_info->repo_) != 0) {
+ errx(1, "git_reference_iterator_new");
+ }
+
+ ReferenceInfo** ris = NULL;
+ size_t ris_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 ReferenceInfo.
+ ReferenceInfo* ri = referenceinfo_create(repo_info->repo_, current);
+ ris = reallocarray(ris, ris_len + 1, sizeof(ReferenceInfo*));
+ if (!ris) {
+ err(1, "reallocarray");
+ }
+ ris[ris_len++] = ri;
+ }
+ git_reference_iterator_free(it);
+ qsort(ris, ris_len, sizeof(ReferenceInfo*), referenceinfo_compare);
+
+ for (size_t i = 0; i < ris_len; i++) {
+ cb(ris[i], user_data);
+ referenceinfo_free(ris[i]);
+ ris[i] = NULL;
+ }
+ free(ris);
+}
+
+bool repoinfo_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[PATH_MAX];
+ if (path[0] == '\0') {
+ estrlcpy(entrypath, entryname, sizeof(entrypath));
+ } else {
+ char full_path[PATH_MAX];
+ path_concat(full_path, sizeof(full_path), path, entryname);
+ estrlcpy(entrypath, full_path, sizeof(entrypath));
+ }
+
+ 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));
+ FileInfo* fileinfo =
+ fileinfo_create(kFileTypeSubmodule, "m---------", entrypath,
+ ".gitmodules", oid_str, -1, -1, "");
+ cb(fileinfo, user_data);
+ fileinfo_free(fileinfo);
+ } else {
+ switch (git_object_type(obj)) {
+ case GIT_OBJECT_BLOB:
+ break;
+ case GIT_OBJECT_TREE: {
+ /* NOTE: recurses */
+ if (!repoinfo_walk_tree_files(repo, (git_tree*)obj, entrypath, cb,
+ user_data)) {
+ git_object_free(obj);
+ return false;
+ }
+ git_object_free(obj);
+ continue;
+ }
+ default:
+ git_object_free(obj);
+ continue;
+ }
+
+ git_blob* blob = (git_blob*)obj;
+ ssize_t size_bytes = git_blob_rawsize(blob);
+ ssize_t size_lines = -1;
+ const char* content = "";
+ 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));
+ FileInfo* fileinfo =
+ fileinfo_create(kFileTypeFile, filemode, entrypath, entrypath, "",
+ size_bytes, size_lines, content);
+ cb(fileinfo, user_data);
+ fileinfo_free(fileinfo);
+ git_object_free(obj);
+ free(filemode);
+ }
+ }
+ return true;
+}
+
+void repoinfo_for_each_file(RepoInfo* repo_info,
+ FileCallback cb,
+ void* user_data) {
+ git_commit* commit = NULL;
+ const git_oid* id = oid_for_spec(repo_info->repo_, "HEAD");
+ if (git_commit_lookup(&commit, repo_info->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 (!repoinfo_walk_tree_files(repo_info->repo_, tree, "", cb, user_data)) {
+ git_tree_free(tree);
+ return;
+ }
+ git_tree_free(tree);
+}
diff --git a/git/repo.h b/git/repo.h
@@ -0,0 +1,45 @@
+#ifndef GITOUT_GIT_REPO_H_
+#define GITOUT_GIT_REPO_H_
+
+#include "git/commit.h"
+#include "git/file.h"
+#include "git/reference.h"
+
+typedef struct {
+ char* name;
+ char* short_name;
+ char* owner;
+ char* description;
+ char* clone_url;
+ const char* submodules;
+ const char* readme;
+ const char* license;
+
+ /* private */
+ void* repo_;
+} RepoInfo;
+
+RepoInfo* repoinfo_create(const char* path);
+void repoinfo_free(RepoInfo* repo_info);
+
+typedef void (*CommitCallback)(const CommitInfo* ci, void* user_data);
+void repoinfo_for_commit(RepoInfo* repo_info,
+ const char* spec,
+ CommitCallback cb,
+ void* user_data);
+
+void repoinfo_for_each_commit(RepoInfo* repo_info,
+ CommitCallback cb,
+ void* user_data);
+
+typedef void (*ReferenceCallback)(const ReferenceInfo* ri, void* user_data);
+void repoinfo_for_each_reference(RepoInfo* repo_info,
+ ReferenceCallback cb,
+ void* user_data);
+
+typedef void (*FileCallback)(const FileInfo* ri, void* user_data);
+void repoinfo_for_each_file(RepoInfo* repo_info,
+ FileCallback cb,
+ void* user_data);
+
+#endif // GITOUT_GIT_REPO_H_
diff --git a/gitout.c b/gitout.c
@@ -0,0 +1,160 @@
+#include "gitout.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/errno.h>
+
+#include "git/commit.h"
+#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 GitoutOptions {
+ 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 */
+};
+
+static void commit_callback(const CommitInfo* ci, void* user_data);
+static void reference_callback(const ReferenceInfo* ri, void* user_data);
+static void file_callback(const FileInfo* fi, void* user_data);
+
+GitoutOptions* gitout_options_create(int argc, const char* argv[]) {
+ GitoutOptions options = {
+ .repodir = NULL,
+ .log_commit_limit = -1,
+ .cachefile_path = NULL,
+ .baseurl = "",
+ };
+ for (int i = 1; i < argc; i++) {
+ if (argv[i][0] != '-') {
+ // Cannot specify more than one repo directory.
+ if (options.repodir) {
+ return NULL;
+ }
+ options.repodir = argv[i];
+ } else if (argv[i][1] == 'c') {
+ // Cache the entries of the log page up to the point of the last commit.
+ // The cachefile will store the last commit id and the entries in the
+ // HTML table. It is up to the user to make sure the state of the
+ // cachefile is in sync with the history of the repository.
+
+ // Mutually exclusive with -l.
+ if (options.log_commit_limit > 0) {
+ return NULL;
+ }
+ // Requires cachefile path argument.
+ if (i + 1 >= argc) {
+ return NULL;
+ }
+ options.cachefile_path = argv[++i];
+ } else if (argv[i][1] == 'l') {
+ // Write a maximum number of commits to the log.html file only. However
+ // the commit files are written as usual.
+
+ // Mutually exclusive with -c option.
+ if (options.cachefile_path) {
+ return NULL;
+ }
+ // Requires log commits argument.
+ if (i + 1 >= argc) {
+ return NULL;
+ }
+ errno = 0;
+ char* p;
+ options.log_commit_limit = strtoll(argv[++i], &p, 10);
+ if (!argv[i][0] || *p || options.log_commit_limit <= 0 || errno) {
+ return NULL;
+ }
+ } else if (argv[i][1] == 'u') {
+ // Requires log commits argument.
+ if (i + 1 >= argc) {
+ return NULL;
+ }
+ options.baseurl = argv[++i];
+ }
+ }
+ // Must specify at least one repo directory.
+ if (!options.repodir) {
+ return NULL;
+ }
+ GitoutOptions* options_out = ecalloc(1, sizeof(GitoutOptions));
+ *options_out = options;
+ return options_out;
+}
+
+void gitout_options_free(GitoutOptions* options) {
+ if (!options) {
+ return;
+ }
+ options->repodir = NULL;
+ options->log_commit_limit = -1;
+ options->cachefile_path = NULL;
+ options->baseurl = NULL;
+ free(options);
+}
+
+void gitout_init(const GitoutOptions* options) {
+ // Restrict our permissions to the minimum necessary.
+ const char* readonly_paths[] = {options->repodir};
+ size_t readonly_paths_count = 1;
+ const char* readwrite_paths[2] = {".", NULL};
+ size_t readwrite_paths_count = 1;
+ if (options->cachefile_path) {
+ readwrite_paths[1] = options->cachefile_path;
+ readwrite_paths_count = 2;
+ }
+ restrict_filesystem_access(readonly_paths, readonly_paths_count,
+ readwrite_paths, readwrite_paths_count);
+ restrict_system_operations(
+ options->cachefile_path != NULL ? kGitoutWithCachefile : kGitout);
+
+ gitout_git_initialize();
+}
+
+void gitout_run(const GitoutOptions* options) {
+ RepoInfo* repo = repoinfo_create(options->repodir);
+ RepoWriter* writer = repowriter_create(kRepoWriterTypeHtml, repo);
+ if (options->log_commit_limit >= 0) {
+ repowriter_set_log_commit_limit(writer, options->log_commit_limit);
+ }
+ if (options->cachefile_path) {
+ repowriter_set_log_cachefile(writer, options->cachefile_path);
+ }
+ if (options->baseurl) {
+ repowriter_set_baseurl(writer, options->baseurl);
+ }
+
+ repowriter_begin(writer);
+ repoinfo_for_each_commit(repo, commit_callback, writer);
+ repoinfo_for_each_reference(repo, reference_callback, writer);
+ repoinfo_for_each_file(repo, file_callback, writer);
+ repowriter_end(writer);
+
+ repowriter_free(writer);
+ repoinfo_free(repo);
+}
+
+void gitout_shutdown(void) {
+ gitout_git_shutdown();
+}
+
+void commit_callback(const CommitInfo* ci, void* user_data) {
+ RepoWriter* writer = (RepoWriter*)user_data;
+ repowriter_add_commit(writer, ci);
+}
+
+void reference_callback(const ReferenceInfo* ri, void* user_data) {
+ RepoWriter* writer = (RepoWriter*)user_data;
+ repowriter_add_reference(writer, ri);
+}
+
+void file_callback(const FileInfo* fi, void* user_data) {
+ RepoWriter* writer = (RepoWriter*)user_data;
+ repowriter_add_file(writer, fi);
+}
diff --git a/gitout.h b/gitout.h
@@ -0,0 +1,12 @@
+#ifndef GITOUT_GITOUT_H_
+#define GITOUT_GITOUT_H_
+
+typedef struct GitoutOptions GitoutOptions;
+GitoutOptions* gitout_options_create(int argc, const char* argv[]);
+void gitout_options_free(GitoutOptions* options);
+
+void gitout_init(const GitoutOptions* options);
+void gitout_run(const GitoutOptions* options);
+void gitout_shutdown(void);
+
+#endif // GITOUT_GITOUT_H_
diff --git a/gitout_index.c b/gitout_index.c
@@ -0,0 +1,64 @@
+#include "gitout_index.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+
+#include "git/git.h"
+#include "git/repo.h"
+#include "security.h"
+#include "utils.h"
+#include "writer/index_writer.h"
+
+struct GitoutIndexOptions {
+ const char** repo_dirs;
+ size_t repo_dir_count;
+};
+
+GitoutIndexOptions* gitout_index_options_create(int argc, const char* argv[]) {
+ GitoutIndexOptions options = {
+ .repo_dirs = NULL,
+ .repo_dir_count = 0,
+ };
+ if (argc > 1) {
+ options.repo_dirs = argv + 1;
+ options.repo_dir_count = argc - 1;
+ }
+ GitoutIndexOptions* options_out = ecalloc(1, sizeof(GitoutIndexOptions));
+ *options_out = options;
+ return options_out;
+}
+
+void gitout_index_options_free(GitoutIndexOptions* options) {
+ if (!options) {
+ return;
+ }
+ free(options);
+}
+
+void gitout_index_init(const GitoutIndexOptions* options) {
+ const char** readonly_paths = options->repo_dirs;
+ size_t readonly_paths_count = options->repo_dir_count;
+ const char* readwrite_paths[2] = {".", NULL};
+ size_t readwrite_paths_count = 1;
+ restrict_filesystem_access(readonly_paths, readonly_paths_count,
+ readwrite_paths, readwrite_paths_count);
+ restrict_system_operations(kGitoutIndex);
+
+ gitout_git_initialize();
+}
+
+void gitout_index_run(const GitoutIndexOptions* options) {
+ IndexWriter* writer = indexwriter_create(kIndexWriterTypeHtml);
+ indexwriter_begin(writer);
+ for (size_t i = 0; i < options->repo_dir_count; i++) {
+ RepoInfo* ri = repoinfo_create(options->repo_dirs[i]);
+ indexwriter_add_repo(writer, ri);
+ repoinfo_free(ri);
+ }
+ indexwriter_end(writer);
+ indexwriter_free(writer);
+}
+
+void gitout_index_shutdown(void) {
+ gitout_git_shutdown();
+}
diff --git a/gitout_index.h b/gitout_index.h
@@ -0,0 +1,12 @@
+#ifndef GITOUT_GITOUT_INDEX_H_
+#define GITOUT_GITOUT_INDEX_H_
+
+typedef struct GitoutIndexOptions GitoutIndexOptions;
+GitoutIndexOptions* gitout_index_options_create(int argc, const char* argv[]);
+void gitout_index_options_free(GitoutIndexOptions* options);
+
+void gitout_index_init(const GitoutIndexOptions* options);
+void gitout_index_run(const GitoutIndexOptions* options);
+void gitout_index_shutdown(void);
+
+#endif // GITOUT_GITOUT_INDEX_H_
diff --git a/gitout_index_main.c b/gitout_index_main.c
@@ -0,0 +1,21 @@
+#include "gitout_index.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+static void gitout_index_usage(const char* program_name) {
+ fprintf(stderr, "usage: %s [repodir...]\n", program_name);
+}
+
+int main(int argc, const char* argv[]) {
+ GitoutIndexOptions* options = gitout_index_options_create(argc, argv);
+ if (options == NULL) {
+ gitout_index_usage(argv[0]);
+ exit(1);
+ }
+ gitout_index_init(options);
+ gitout_index_run(options);
+ gitout_index_shutdown();
+ gitout_index_options_free(options);
+ return 0;
+}
diff --git a/gitout_main.c b/gitout_main.c
@@ -0,0 +1,23 @@
+#include "gitout.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+static void gitout_usage(const char* program_name) {
+ fprintf(stderr,
+ "usage: %s [-c cachefile | -l commits] [-u baseurl] repodir\n",
+ program_name);
+}
+
+int main(int argc, const char* argv[]) {
+ GitoutOptions* options = gitout_options_create(argc, argv);
+ if (options == NULL) {
+ gitout_usage(argv[0]);
+ exit(1);
+ }
+ gitout_init(options);
+ gitout_run(options);
+ gitout_shutdown();
+ gitout_options_free(options);
+ return 0;
+}
diff --git a/secondary/third_party/googletest/BUILD.gn b/secondary/third_party/googletest/BUILD.gn
@@ -0,0 +1,384 @@
+config("gtest_private_config") {
+ visibility = [ ":*" ]
+ include_dirs = [ "googletest" ]
+ cflags_cc = [
+ "-Wno-covered-switch-default",
+ "-Wno-deprecated",
+ "-Wno-double-promotion",
+ "-Wno-exit-time-destructors",
+ "-Wno-float-equal",
+ "-Wno-gnu-zero-variadic-macro-arguments",
+ "-Wno-missing-prototypes",
+ "-Wno-missing-variable-declarations",
+ "-Wno-old-style-cast",
+ "-Wno-sign-conversion",
+ "-Wno-switch-enum",
+ "-Wno-unreachable-code-break",
+ "-Wno-unused-member-function",
+ "-Wno-unused-private-field",
+ "-Wno-unused-template",
+ "-Wno-used-but-marked-unused",
+ "-Wno-zero-as-null-pointer-constant",
+ "-Wno-extra-semi-stmt",
+ ]
+ ldflags = [ "-lpthread" ]
+}
+
+config("gtest_config") {
+ include_dirs = [ "googletest/include" ]
+ cflags_cc = [
+ "-Wno-global-constructors",
+ "-Wno-missing-noreturn",
+ "-Wno-padded",
+ "-Wno-shift-sign-overflow",
+ "-Wno-thread-safety", # googletest lacks locking annotations.
+ "-Wno-undef",
+ "-Wno-weak-vtables",
+ ]
+ ldflags = [ "-lpthread" ]
+}
+
+source_set("gtest_internal_headers") {
+ visibility = [ ":*" ]
+ testonly = true
+ sources = [
+ "googletest/include/gtest/gtest-assertion-result.h",
+ "googletest/include/gtest/gtest-death-test.h",
+ "googletest/include/gtest/gtest-matchers.h",
+ "googletest/include/gtest/gtest-message.h",
+ "googletest/include/gtest/gtest-param-test.h",
+ "googletest/include/gtest/gtest-printers.h",
+ "googletest/include/gtest/gtest-spi.h",
+ "googletest/include/gtest/gtest-test-part.h",
+ "googletest/include/gtest/gtest-typed-test.h",
+ "googletest/include/gtest/gtest_pred_impl.h",
+ "googletest/include/gtest/gtest_prod.h",
+ "googletest/include/gtest/internal/custom/gtest-port.h",
+ "googletest/include/gtest/internal/custom/gtest-printers.h",
+ "googletest/include/gtest/internal/custom/gtest.h",
+ "googletest/include/gtest/internal/gtest-death-test-internal.h",
+ "googletest/include/gtest/internal/gtest-filepath.h",
+ "googletest/include/gtest/internal/gtest-internal.h",
+ "googletest/include/gtest/internal/gtest-param-util.h",
+ "googletest/include/gtest/internal/gtest-port-arch.h",
+ "googletest/include/gtest/internal/gtest-port.h",
+ "googletest/include/gtest/internal/gtest-string.h",
+ "googletest/include/gtest/internal/gtest-type-util.h",
+ "googletest/src/gtest-internal-inl.h",
+ ]
+}
+
+source_set("gtest_test_headers") {
+ visibility = [ ":*" ]
+ testonly = true
+ sources = [ "googletest/include/gtest/gtest-spi.h" ]
+}
+
+static_library("gtest") {
+ testonly = true
+ public = [ "googletest/include/gtest/gtest.h" ]
+ sources = [
+ "googletest/src/gtest-all.cc",
+ "googletest/src/gtest-assertion-result.cc",
+ "googletest/src/gtest-death-test.cc",
+ "googletest/src/gtest-filepath.cc",
+ "googletest/src/gtest-matchers.cc",
+ "googletest/src/gtest-port.cc",
+ "googletest/src/gtest-printers.cc",
+ "googletest/src/gtest-test-part.cc",
+ "googletest/src/gtest-typed-test.cc",
+ "googletest/src/gtest.cc",
+ ]
+ sources -= [ "googletest/src/gtest-all.cc" ]
+ public_configs = [ ":gtest_config" ]
+ configs += [ ":gtest_private_config" ]
+ deps = [
+ ":gtest_internal_headers",
+ ":gtest_test_headers",
+ ]
+}
+
+static_library("gtest_main") {
+ testonly = true
+ sources = [ "googletest/src/gtest_main.cc" ]
+ public_deps = [ ":gtest" ]
+}
+
+executable("gtest_all_test") {
+ testonly = true
+ sources = [
+ "googletest/test/googletest-death-test-test.cc",
+ "googletest/test/googletest-filepath-test.cc",
+ "googletest/test/googletest-message-test.cc",
+ "googletest/test/googletest-options-test.cc",
+ "googletest/test/googletest-param-test2-test.cc",
+ "googletest/test/googletest-port-test.cc",
+ "googletest/test/googletest-printers-test.cc",
+ "googletest/test/googletest-test-part-test.cc",
+ "googletest/test/gtest-typed-test2_test.cc",
+ "googletest/test/gtest-typed-test_test.cc",
+ "googletest/test/gtest_dirs_test.cc",
+ "googletest/test/gtest_main_unittest.cc",
+ "googletest/test/gtest_pred_impl_unittest.cc",
+ "googletest/test/gtest_skip_test.cc",
+ "googletest/test/gtest_sole_header_test.cc",
+ "googletest/test/gtest_unittest.cc",
+ ]
+ configs += [ ":gtest_private_config" ]
+ deps = [
+ ":gtest",
+ ":gtest_internal_headers",
+ ":gtest_main",
+ ":gtest_test_headers",
+ ]
+}
+
+executable("gtest_environment_test") {
+ testonly = true
+ sources = [ "googletest/test/gtest_environment_test.cc" ]
+ configs += [ ":gtest_private_config" ]
+ deps = [
+ ":gtest",
+ ":gtest_internal_headers",
+ ]
+}
+
+executable("gtest_listener_test") {
+ testonly = true
+ sources = [ "googletest/test/googletest-listener-test.cc" ]
+ deps = [ ":gtest" ]
+ cflags_cc = [
+ "-Wno-missing-prototypes",
+ "-Wno-missing-variable-declarations",
+ ]
+}
+
+executable("gtest_no_test") {
+ testonly = true
+ sources = [ "googletest/test/gtest_no_test_unittest.cc" ]
+ deps = [ ":gtest" ]
+}
+
+executable("gtest_param_test") {
+ testonly = true
+ sources = [
+ "googletest/test/googletest-param-test-test.cc",
+ "googletest/test/googletest-param-test-test.h",
+ "googletest/test/googletest-param-test2-test.cc",
+ ]
+ configs += [ ":gtest_private_config" ]
+ deps = [ ":gtest" ]
+}
+
+executable("gtest_premature_exit_test") {
+ testonly = true
+ sources = [ "googletest/test/gtest_premature_exit_test.cc" ]
+ configs += [ ":gtest_private_config" ]
+ deps = [ ":gtest" ]
+}
+
+executable("gtest_repeat_test") {
+ testonly = true
+ sources = [ "googletest/test/gtest_repeat_test.cc" ]
+ configs += [ ":gtest_private_config" ]
+ deps = [
+ ":gtest",
+ ":gtest_internal_headers",
+ ]
+}
+
+executable("gtest_sole_header_test") {
+ testonly = true
+ sources = [ "googletest/test/gtest_sole_header_test.cc" ]
+ deps = [
+ ":gtest",
+ ":gtest_main",
+ ]
+}
+
+executable("gtest_stress_test") {
+ testonly = true
+ sources = [ "googletest/test/gtest_stress_test.cc" ]
+ configs += [ ":gtest_private_config" ]
+ deps = [
+ ":gtest",
+ ":gtest_internal_headers",
+ ]
+}
+
+executable("gtest_unittest_api_test") {
+ testonly = true
+ sources = [ "googletest/test/gtest-unittest-api_test.cc" ]
+ configs += [ ":gtest_private_config" ]
+ deps = [ ":gtest" ]
+}
+
+group("gtest_all_tests") {
+ testonly = true
+ deps = [
+ ":gtest_all_test",
+ ":gtest_environment_test",
+ ":gtest_listener_test",
+ ":gtest_no_test",
+ ":gtest_param_test",
+ ":gtest_premature_exit_test",
+ ":gtest_repeat_test",
+ ":gtest_sole_header_test",
+ ":gtest_stress_test",
+ ":gtest_unittest_api_test",
+ ]
+}
+
+config("gmock_private_config") {
+ visibility = [ ":*" ]
+ include_dirs = [ "googlemock" ]
+ cflags_cc = [
+ "-Wno-deprecated",
+ "-Wno-double-promotion",
+ "-Wno-exit-time-destructors",
+ "-Wno-float-equal",
+ "-Wno-missing-prototypes",
+ "-Wno-pedantic",
+ "-Wno-sign-conversion",
+ "-Wno-switch-enum",
+ "-Wno-unused-macros",
+ "-Wno-unused-parameter",
+ "-Wno-used-but-marked-unused",
+ "-Wno-zero-as-null-pointer-constant",
+ ]
+}
+
+config("gmock_config") {
+ include_dirs = [ "googlemock/include" ]
+
+ cflags_cc = [
+ # The MOCK_METHODn() macros do not specify "override", which triggers this
+ # warning in users: "error: 'Method' overrides a member function but is not
+ # marked 'override' [-Werror,-Winconsistent-missing-override]". Suppress
+ # these warnings until https://github.com/google/googletest/issues/533 is
+ # fixed.
+ "-Wno-inconsistent-missing-override",
+ ]
+ ldflags = [ "-lpthread" ]
+}
+
+source_set("gmock_internal_headers") {
+ visibility = [ ":*" ]
+ testonly = true
+ sources = [
+ "googlemock/include/gmock/gmock-actions.h",
+ "googlemock/include/gmock/gmock-cardinalities.h",
+ "googlemock/include/gmock/gmock-generated-actions.h",
+ "googlemock/include/gmock/gmock-generated-function-mockers.h",
+ "googlemock/include/gmock/gmock-generated-matchers.h",
+ "googlemock/include/gmock/gmock-generated-nice-strict.h",
+ "googlemock/include/gmock/gmock-matchers.h",
+ "googlemock/include/gmock/gmock-more-actions.h",
+ "googlemock/include/gmock/gmock-more-matchers.h",
+ "googlemock/include/gmock/gmock-spec-builders.h",
+ "googlemock/include/gmock/internal/custom/gmock-generated-actions.h",
+ "googlemock/include/gmock/internal/custom/gmock-matchers.h",
+ "googlemock/include/gmock/internal/custom/gmock-port.h",
+ "googlemock/include/gmock/internal/gmock-generated-internal-utils.h",
+ "googlemock/include/gmock/internal/gmock-internal-utils.h",
+ "googlemock/include/gmock/internal/gmock-port.h",
+ ]
+}
+
+static_library("gmock") {
+ testonly = true
+ public = [ "googlemock/include/gmock/gmock.h" ]
+ sources = [
+ "googlemock/src/gmock-all.cc",
+ "googlemock/src/gmock-cardinalities.cc",
+ "googlemock/src/gmock-internal-utils.cc",
+ "googlemock/src/gmock-matchers.cc",
+ "googlemock/src/gmock-spec-builders.cc",
+ "googlemock/src/gmock.cc",
+ ]
+ sources -= [ "googlemock/src/gmock-all.cc" ]
+ public_configs = [ ":gmock_config" ]
+ configs += [ ":gmock_private_config" ]
+ deps = [
+ ":gmock_internal_headers",
+ ":gtest",
+ ":gtest_internal_headers",
+ ]
+}
+
+static_library("gmock_main") {
+ testonly = true
+ sources = [ "googlemock/src/gmock_main.cc" ]
+ configs += [ ":gmock_private_config" ]
+ public_deps = [
+ ":gmock",
+ ":gtest",
+ ]
+}
+
+executable("gmock_all_test") {
+ testonly = true
+ sources = [
+ "googlemock/test/gmock-actions_test.cc",
+ "googlemock/test/gmock-cardinalities_test.cc",
+ "googlemock/test/gmock-generated-actions_test.cc",
+ "googlemock/test/gmock-generated-function-mockers_test.cc",
+ "googlemock/test/gmock-generated-internal-utils_test.cc",
+ "googlemock/test/gmock-generated-matchers_test.cc",
+ "googlemock/test/gmock-internal-utils_test.cc",
+ "googlemock/test/gmock-matchers_test.cc",
+ "googlemock/test/gmock-more-actions_test.cc",
+ "googlemock/test/gmock-nice-strict_test.cc",
+ "googlemock/test/gmock-port_test.cc",
+ "googlemock/test/gmock-spec-builders_test.cc",
+ "googlemock/test/gmock_test.cc",
+ ]
+ configs += [
+ ":gmock_private_config",
+ ":gtest_private_config",
+ ]
+ deps = [
+ ":gmock",
+ ":gmock_internal_headers",
+ ":gmock_main",
+ ":gtest",
+ ":gtest_internal_headers",
+ ":gtest_test_headers",
+ ]
+}
+
+executable("gmock_link_test") {
+ testonly = true
+ sources = [
+ "googlemock/test/gmock_link2_test.cc",
+ "googlemock/test/gmock_link_test.cc",
+ "googlemock/test/gmock_link_test.h",
+ ]
+ configs += [ ":gmock_private_config" ]
+ deps = [
+ ":gmock",
+ ":gmock_main",
+ ":gtest",
+ ":gtest_internal_headers",
+ ]
+}
+
+executable("gmock_stress_test") {
+ testonly = true
+ sources = [ "googlemock/test/gmock_stress_test.cc" ]
+ cflags_cc = [ "-Wno-unused-member-function" ]
+ configs += [ ":gmock_private_config" ]
+ deps = [
+ ":gmock",
+ ":gtest",
+ ]
+}
+
+group("gmock_all_tests") {
+ testonly = true
+ deps = [
+ ":gmock_all_test",
+ ":gmock_link_test",
+ ":gmock_stress_test",
+ ]
+}
diff --git a/security.c b/security.c
@@ -0,0 +1,53 @@
+#include "security.h"
+
+#include <err.h>
+
+#ifdef __OpenBSD__
+#include <unistd.h>
+#else
+static int unveil(const char* path, const char* permissions) {
+ return (path && permissions) ? 0 : 1;
+}
+static int pledge(const char* promises, const char* execpromises) {
+ return (promises && execpromises) ? 0 : 1;
+}
+#endif // __OpenBSD__
+
+void restrict_filesystem_access(const char* readonly_paths[],
+ size_t readonly_paths_count,
+ const char* readwrite_paths[],
+ size_t readwrite_paths_count) {
+ for (size_t i = 0; i < readonly_paths_count; i++) {
+ const char* path = readonly_paths[i];
+ if (unveil(path, "r") == -1) {
+ err(1, "unveil: %s", path);
+ }
+ }
+ for (size_t i = 0; i < readwrite_paths_count; i++) {
+ const char* path = readwrite_paths[i];
+ if (unveil(path, "rwc") == -1) {
+ err(1, "unveil: %s", path);
+ }
+ }
+}
+
+void restrict_system_operations(RestrictionType type) {
+ const char* promises = NULL;
+ switch (type) {
+ case kGitout:
+ promises = "stdio rpath wpath cpath";
+ break;
+ case kGitoutWithCachefile:
+ promises = "stdio rpath wpath cpath fattr";
+ break;
+ case kGitoutIndex:
+ promises = "stdio rpath";
+ break;
+ default:
+ err(1, "unknown restriction");
+ break;
+ }
+ if (pledge(promises, NULL) == -1) {
+ err(1, "pledge");
+ }
+}
diff --git a/security.h b/security.h
@@ -0,0 +1,25 @@
+#ifndef GITOUT_SECURITY_H_
+#define GITOUT_SECURITY_H_
+
+#include <stdlib.h>
+
+// Limit access to only the specified paths.
+//
+// No effect on OSes other than OpenBSD.
+void restrict_filesystem_access(const char* readonly_paths[],
+ size_t readonly_paths_count,
+ const char* readwrite_paths[],
+ size_t readwrite_paths_count);
+
+typedef enum {
+ kGitout,
+ kGitoutWithCachefile,
+ kGitoutIndex,
+} RestrictionType;
+
+// Limits system operations to the minimum required.
+//
+// No effect on OSes other than OpenBSD.
+void restrict_system_operations(RestrictionType type);
+
+#endif // GITOUT_SECURITY_H_
diff --git a/third_party/googletest b/third_party/googletest
@@ -0,0 +1 @@
+Subproject commit 519beb0e52c842729b4b53731d27c0e0c32ab4a2
diff --git a/third_party/openbsd/BUILD.gn b/third_party/openbsd/BUILD.gn
@@ -0,0 +1,32 @@
+config("openbsd_config") {
+ public_configs = [
+ "//build:compiler_std",
+ "//build:compiler_warnings",
+ "//build:strict_prototypes",
+ ]
+ if (is_debug) {
+ public_configs += [
+ "//build:debug",
+ "//build:no_optimize",
+ "//build:symbols",
+ ]
+ } else {
+ public_configs += [
+ "//build:release",
+ "//build:optimize_size",
+ "//build:lto",
+ ]
+ }
+}
+
+source_set("openbsd") {
+ sources = [
+ "reallocarray.c",
+ "reallocarray.h",
+ "strlcat.c",
+ "strlcat.h",
+ "strlcpy.c",
+ "strlcpy.h",
+ ]
+ configs += [ ":openbsd_config" ]
+}
diff --git a/third_party/openbsd/reallocarray.c b/third_party/openbsd/reallocarray.c
@@ -0,0 +1,37 @@
+/* $OpenBSD: reallocarray.c,v 1.3 2015/09/13 08:31:47 guenther Exp $ */
+/*
+ * Copyright (c) 2008 Otto Moerbeek <otto@drijf.net>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/errno.h>
+
+#include "reallocarray.h"
+
+/*
+ * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX
+ * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW
+ */
+#define MUL_NO_OVERFLOW ((size_t)1 << (sizeof(size_t) * 4))
+
+void* reallocarray(void* optr, size_t nmemb, size_t size) {
+ if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && nmemb > 0 &&
+ SIZE_MAX / nmemb < size) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ return realloc(optr, size * nmemb);
+}
diff --git a/third_party/openbsd/reallocarray.h b/third_party/openbsd/reallocarray.h
@@ -0,0 +1,9 @@
+#ifndef OPENBSD_REALLOCARRAY_H_
+#define OPENBSD_REALLOCARRAY_H_
+
+#include <stdlib.h>
+
+#undef reallocarray
+void* reallocarray(void* optr, size_t nmemb, size_t size);
+
+#endif // OPENBSD_REALLOCARRAY_H_
diff --git a/third_party/openbsd/strlcat.c b/third_party/openbsd/strlcat.c
@@ -0,0 +1,57 @@
+
+/* $OpenBSD: strlcat.c,v 1.15 2015/03/02 21:41:08 millert Exp $ */
+
+/*
+ * Copyright (c) 1998, 2015 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <string.h>
+
+#include "strlcat.h"
+
+/*
+ * Appends src to string dst of size dsize (unlike strncat, dsize is the
+ * full size of dst, not space left). At most dsize-1 characters
+ * will be copied. Always NUL terminates (unless dsize <= strlen(dst)).
+ * Returns strlen(src) + MIN(dsize, strlen(initial dst)).
+ * If retval >= dsize, truncation occurred.
+ */
+size_t strlcat(char* dst, const char* src, size_t dsize) {
+ const char* odst = dst;
+ const char* osrc = src;
+ size_t n = dsize;
+ size_t dlen;
+
+ /* Find the end of dst and adjust bytes left but don't go past end. */
+ while (n-- != 0 && *dst != '\0') {
+ dst++;
+ }
+ dlen = dst - odst;
+ n = dsize - dlen;
+
+ if (n-- == 0) {
+ return (dlen + strlen(src));
+ }
+ while (*src != '\0') {
+ if (n != 0) {
+ *dst++ = *src;
+ n--;
+ }
+ src++;
+ }
+ *dst = '\0';
+
+ return (dlen + (src - osrc)); /* count does not include NUL */
+}
diff --git a/third_party/openbsd/strlcat.h b/third_party/openbsd/strlcat.h
@@ -0,0 +1,9 @@
+#ifndef OPENBSD_STRLCAT_H_
+#define OPENBSD_STRLCAT_H_
+
+#include <stdlib.h>
+
+#undef strlcat
+size_t strlcat(char* dst, const char* src, size_t dsize);
+
+#endif // OPENBSD_STRLCAT_H_
diff --git a/third_party/openbsd/strlcpy.c b/third_party/openbsd/strlcpy.c
@@ -0,0 +1,51 @@
+/* $OpenBSD: strlcpy.c,v 1.12 2015/01/15 03:54:12 millert Exp $ */
+
+/*
+ * Copyright (c) 1998, 2015 Todd C. Miller <Todd.Miller@courtesan.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <string.h>
+
+#include "strlcpy.h"
+
+/*
+ * Copy string src to buffer dst of size dsize. At most dsize-1
+ * chars will be copied. Always NUL terminates (unless dsize == 0).
+ * Returns strlen(src); if retval >= dsize, truncation occurred.
+ */
+size_t strlcpy(char* dst, const char* src, size_t dsize) {
+ const char* osrc = src;
+ size_t nleft = dsize;
+
+ /* Copy as many bytes as will fit. */
+ if (nleft != 0) {
+ while (--nleft != 0) {
+ if ((*dst++ = *src++) == '\0') {
+ break;
+ }
+ }
+ }
+
+ /* Not enough room in dst, add NUL and traverse rest of src. */
+ if (nleft == 0) {
+ if (dsize != 0) {
+ *dst = '\0'; /* NUL-terminate dst */
+ }
+ while (*src++) {
+ }
+ }
+
+ return (src - osrc - 1); /* count does not include NUL */
+}
diff --git a/third_party/openbsd/strlcpy.h b/third_party/openbsd/strlcpy.h
@@ -0,0 +1,9 @@
+#ifndef OPENBSD_STRLCPY_H_
+#define OPENBSD_STRLCPY_H_
+
+#include <stdlib.h>
+
+#undef strlcpy
+size_t strlcpy(char* dst, const char* src, size_t dsize);
+
+#endif // OPENBSD_STRLCPY_H_
diff --git a/utils.c b/utils.c
@@ -0,0 +1,100 @@
+#include "utils.h"
+
+#include <err.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "third_party/openbsd/strlcat.h"
+#include "third_party/openbsd/strlcpy.h"
+
+char* path_concat(char* out, size_t out_len, const char* p1, const char* p2) {
+ size_t p1_len = strlen(p1);
+ size_t p2_len = strlen(p2);
+ if (p1_len == 0) {
+ estrlcpy(out, p2, out_len);
+ return out;
+ }
+ if (p2_len == 0) {
+ estrlcpy(out, p1, out_len);
+ return out;
+ }
+ if (p1_len + p2_len + 2 > out_len) {
+ errx(1, "path truncated. p1: '%s' p2: '%s'", p1, p2);
+ }
+ if (snprintf(out, out_len, "%s/%s", p1, p2) < 0) {
+ err(1, "snprintf");
+ }
+ return out;
+}
+
+int mkdirp(const char* path) {
+ char mut_path[PATH_MAX];
+ estrlcpy(mut_path, path, sizeof(mut_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) {
+ return -1;
+ }
+ *p = '/';
+ }
+ if (mkdir(mut_path, S_IRWXU | S_IRWXG | S_IRWXO) < 0 && errno != EEXIST) {
+ return -1;
+ }
+ return 0;
+}
+
+void* ecalloc(size_t count, size_t size) {
+ void* ptr = calloc(count, size);
+ if (!ptr) {
+ err(1, "calloc");
+ }
+ return ptr;
+}
+
+char* estrdup(const char* s) {
+ char* out_str = strdup(s);
+ if (!out_str) {
+ err(1, "strcpy");
+ }
+ return out_str;
+}
+
+size_t estrlcpy(char* dst, const char* src, size_t dsize) {
+ size_t len = strlcpy(dst, src, dsize);
+ if (len >= dsize) {
+ errx(1, "string truncated: '%s'", src);
+ }
+ return len;
+}
+
+size_t estrlcat(char* dst, const char* src, size_t dsize) {
+ size_t len = strlcat(dst, src, dsize);
+ if (len >= dsize) {
+ errx(1, "string truncated: '%s'", src);
+ }
+ return len;
+}
+
+FILE* efopen(const char* filename, const char* flags) {
+ FILE* fp = fopen(filename, flags);
+ if (!fp) {
+ err(1, "fopen: '%s'", filename);
+ }
+ return fp;
+}
+
+void checkfileerror(FILE* fp, const char* name, char mode) {
+ if (mode == 'r' && ferror(fp)) {
+ errx(1, "read error: %s", name);
+ } else if (mode == 'w' && (fflush(fp) || ferror(fp))) {
+ errx(1, "write error: %s", name);
+ }
+}
diff --git a/utils.h b/utils.h
@@ -0,0 +1,30 @@
+#ifndef GITOUT_UTILS_H_
+#define GITOUT_UTILS_H_
+
+#include <stdio.h>
+
+/* Concatenates path parts to "p1/p2". Exits on failure or truncation. */
+char* path_concat(char* out, size_t out_len, 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);
+
+/* Behaves as strdup but exits on failure. */
+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);
+
+/* Behaves as strlcat but exits on failure or truncation. */
+size_t estrlcat(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);
+
+/* Exits with error if the specified FILE* has an error. */
+void checkfileerror(FILE* fp, const char* name, char mode);
+
+#endif // GITOUT_UTILS_H_
diff --git a/utils_test.cc b/utils_test.cc
@@ -0,0 +1,29 @@
+extern "C" {
+#include "utils.h"
+}
+
+#include <limits.h>
+#include <string.h>
+
+#include "gtest/gtest.h"
+
+TEST(Utils, CanConcatenatePaths) {
+ char out[PATH_MAX];
+ const char* returned = path_concat(out, sizeof(out), "p1", "p2");
+ EXPECT_EQ(strcmp(out, "p1/p2"), 0);
+ EXPECT_EQ(strcmp(returned, "p1/p2"), 0);
+}
+
+TEST(Utils, CanConcatenatePathsFirstEmpty) {
+ char out[PATH_MAX];
+ const char* returned = path_concat(out, sizeof(out), "", "p2");
+ EXPECT_EQ(strcmp(out, "p2"), 0);
+ EXPECT_EQ(strcmp(returned, "p2"), 0);
+}
+
+TEST(Utils, CanConcatenatePathsSecondEmpty) {
+ char out[PATH_MAX];
+ const char* returned = path_concat(out, sizeof(out), "p1", "");
+ EXPECT_EQ(strcmp(out, "p1"), 0);
+ EXPECT_EQ(strcmp(returned, "p1"), 0);
+}
diff --git a/writer/BUILD.gn b/writer/BUILD.gn
@@ -0,0 +1,19 @@
+source_set("index_writer") {
+ sources = [
+ "index_writer.c",
+ "index_writer.h",
+ ]
+ configs += [ "//:gitout_config" ]
+ deps = [ "//writer/html:index_writer" ]
+ public_deps = [ "//git" ]
+}
+
+source_set("repo_writer") {
+ sources = [
+ "repo_writer.c",
+ "repo_writer.h",
+ ]
+ configs += [ "//:gitout_config" ]
+ deps = [ "//writer/html:repo_writer" ]
+ public_deps = [ "//git" ]
+}
diff --git a/writer/atom/BUILD.gn b/writer/atom/BUILD.gn
@@ -0,0 +1,12 @@
+source_set("atom") {
+ sources = [
+ "atom.c",
+ "atom.h",
+ ]
+ configs += [ "//:gitout_config" ]
+ deps = [
+ "//:format",
+ "//:utils",
+ ]
+ public_deps = [ "//git" ]
+}
diff --git a/writer/atom/atom.c b/writer/atom/atom.c
@@ -0,0 +1,113 @@
+#include "atom.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "format.h"
+#include "utils.h"
+
+struct Atom {
+ const RepoInfo* repo;
+ const char* baseurl;
+ FILE* out;
+ size_t remaining_commits;
+};
+
+Atom* atom_create(const RepoInfo* repo, AtomType type) {
+ 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->remaining_commits = 100;
+ return atom;
+}
+
+void atom_free(Atom* atom) {
+ fclose(atom->out);
+ atom->out = NULL;
+ free(atom);
+}
+
+void atom_set_baseurl(Atom* atom, const char* baseurl) {
+ atom->baseurl = baseurl;
+}
+
+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, atom->repo->short_name);
+ fprintf(atom->out, ", branch HEAD</title>\n");
+ fprintf(atom->out, "<subtitle>");
+ print_xml_encoded(atom->out, atom->repo->description);
+ fprintf(atom->out, "</subtitle>\n");
+}
+
+void atom_add_commit(Atom* atom,
+ const CommitInfo* ci,
+ const char* path,
+ const char* content_type,
+ const char* tag) {
+ if (atom->remaining_commits == 0) {
+ return;
+ }
+ atom->remaining_commits--;
+
+ fprintf(atom->out, "<entry>\n");
+ fprintf(atom->out, "<id>%s</id>\n", ci->oid);
+
+ fprintf(atom->out, "<published>");
+ print_time_z(atom->out, ci->author_time);
+ fprintf(atom->out, "</published>\n");
+
+ fprintf(atom->out, "<updated>");
+ print_time_z(atom->out, ci->commit_time);
+ fprintf(atom->out, "</updated>\n");
+
+ if (ci->summary) {
+ fprintf(atom->out, "<title>");
+ if (tag && tag[0] != '\0') {
+ fputc('[', atom->out);
+ print_xml_encoded(atom->out, tag);
+ fputc(']', atom->out);
+ }
+ print_xml_encoded(atom->out, ci->summary);
+ fprintf(atom->out, "</title>\n");
+ }
+ fprintf(atom->out, "<link rel=\"alternate\" ");
+ if (strlen(content_type) > 0) {
+ fprintf(atom->out, "type=\"%s\" ", content_type);
+ }
+ fprintf(atom->out, "href=\"%s%s\" />\n", atom->baseurl, path);
+
+ fprintf(atom->out, "<author>\n<name>");
+ print_xml_encoded(atom->out, ci->author_name);
+ fprintf(atom->out, "</name>\n<email>");
+ print_xml_encoded(atom->out, ci->author_email);
+ fprintf(atom->out, "</email>\n</author>\n");
+
+ fprintf(atom->out, "<content>");
+ fprintf(atom->out, "commit %s\n", ci->oid);
+ if (ci->parentoid[0]) {
+ fprintf(atom->out, "parent %s\n", ci->parentoid);
+ }
+ fprintf(atom->out, "Author: ");
+ print_xml_encoded(atom->out, ci->author_name);
+ fprintf(atom->out, " <");
+ print_xml_encoded(atom->out, ci->author_email);
+ fprintf(atom->out, ">\n");
+ fprintf(atom->out, "Date: ");
+ print_time(atom->out, ci->author_time, ci->author_timezone_offset);
+ fprintf(atom->out, "\n");
+ if (ci->msg) {
+ fputc('\n', atom->out);
+ print_xml_encoded(atom->out, ci->msg);
+ }
+ fprintf(atom->out, "\n</content>\n</entry>\n");
+}
+
+void atom_end(Atom* atom) {
+ fprintf(atom->out, "</feed>\n");
+}
diff --git a/writer/atom/atom.h b/writer/atom/atom.h
@@ -0,0 +1,40 @@
+#ifndef GITOUT_WRITER_ATOM_ATOM_H_
+#define GITOUT_WRITER_ATOM_ATOM_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 RepoInfo* repo, AtomType type);
+
+/* Frees the specified Atom RSS output file. */
+void atom_free(Atom* atom);
+
+/* Sets the base URL for the RSS feed. Example: "https://example.com/git/". */
+void atom_set_baseurl(Atom* atom, const char* baseurl);
+
+/* Writes the Atom RSS header including <feed>, <title>, <subtitle>. */
+void atom_begin(Atom* atom);
+
+/* Writes an RSS <entry> for the commit. */
+void atom_add_commit(Atom* atom,
+ const CommitInfo* ci,
+ const char* path,
+ const char* content_type,
+ const char* tag);
+
+/* Closes out the Atom <feed>. */
+void atom_end(Atom* atom);
+
+#endif // GITOUT_WRITER_ATOM_ATOM_H_
diff --git a/writer/cache/BUILD.gn b/writer/cache/BUILD.gn
@@ -0,0 +1,9 @@
+source_set("cache") {
+ sources = [
+ "cache.c",
+ "cache.h",
+ ]
+ configs += [ "//:gitout_config" ]
+ deps = [ "//:utils" ]
+ public_deps = [ "//git" ]
+}
diff --git a/writer/cache/cache.c b/writer/cache/cache.c
@@ -0,0 +1,158 @@
+#include "cache.h"
+
+#include <err.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "git/git.h"
+#include "utils.h"
+
+struct Cache {
+ bool can_add_commits;
+ char* cache_path;
+ char* temp_cache_path;
+ WriteCommitRow write_commit_row;
+ FILE* cache_in;
+ FILE* cache_out;
+ char* lastoid_in;
+ 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 = 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;
+
+ // Open previous cache for reading, if it exists, and read lastoid.
+ 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];
+ char* oid_str = fgets(buf, sizeof(buf), cache->cache_in);
+ if (!oid_str) {
+ err(1, "fgets");
+ }
+ estrlcpy(cache->lastoid_in, oid_str, sizeof(buf));
+ }
+ 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;
+}
+
+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);
+}
+
+bool cache_can_add_commits(const Cache* cache) {
+ return cache->can_add_commits;
+}
+
+void cache_add_commit_row(Cache* cache, const CommitInfo* ci) {
+ // The first row of the file is the last OID.
+ if (!cache->wrote_lastoid_out) {
+ fprintf(cache->cache_out, "%s\n", ci->oid);
+ cache->wrote_lastoid_out = true;
+ }
+ // If all commits are already written; do nothing.
+ if (!cache->can_add_commits ||
+ strncmp(ci->oid, cache->lastoid_in, kOidLen - 1) == 0) {
+ cache->can_add_commits = false;
+ return;
+ }
+ // The rest of the file is the log contents up to and including that commit.
+ cache->write_commit_row(cache->cache_out, ci);
+}
+
+void cache_write(Cache* cache) {
+ if (cache->cache_in) {
+ // If we didn't write any records, copy the previous cache lastoid.
+ if (!cache->wrote_lastoid_out) {
+ fprintf(cache->cache_out, "%s\n", cache->lastoid_in);
+ cache->wrote_lastoid_out = true;
+ }
+ // append previous cached log to new cached log.
+ char buf[BUFSIZ];
+ while (!feof(cache->cache_in)) {
+ size_t n = fread(buf, 1, sizeof(buf), cache->cache_in);
+ if (ferror(cache->cache_in)) {
+ break;
+ }
+ if (fwrite(buf, 1, n, cache->cache_out) != n) {
+ 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");
+
+ // Copy the log lines to out.
+ char* line = NULL;
+ size_t len = 0;
+ bool is_sha = true;
+ while (getline(&line, &len, fcache) != -1) {
+ // Skip the first line containing the commit SHA.
+ if (is_sha) {
+ is_sha = false;
+ continue;
+ }
+ fprintf(out, "%s", line);
+ }
+ free(line);
+
+ fclose(fcache);
+}
diff --git a/writer/cache/cache.h b/writer/cache/cache.h
@@ -0,0 +1,79 @@
+#ifndef GITOUT_WRITER_CACHE_CACHE_H_
+#define GITOUT_WRITER_CACHE_CACHE_H_
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "git/commit.h"
+
+/* Commit log cache file.
+ *
+ * A cache consists of two files:
+ * - An existing cache, if it exists, which is read from cache_path at the
+ * start of a run.
+ * - An updated cache, written to a temp file, which overwrites the existing
+ * cache on invocation of cache_write.
+ *
+ * The fomat of a cache is:
+ * LAST_CACHED_OID
+ * FORMATTED_OID_ROW for LAST_CACHED_OID
+ * FORMATTED_OID_ROW for LAST_CACHED_OID~1
+ * ...
+ * FORMATTED_OID_ROW for LAST_CACHED_OID~N
+ *
+ * Git OIDs are processed from most recent (HEAD) to least recent and added to
+ * the cache via cache_add_commit_row. When LAST_CACHE_OID from the previous
+ * cache (if it exists) is processed by cache_add_commit_row, the cache will
+ * return false for all further invocations of cache_can_add_commits and no
+ * further commits can be added.
+ *
+ * When complete, the cache can be written to the cache_path by invoking
+ * cache_write. The cache should then be freed with cache_free.
+ */
+typedef struct Cache Cache;
+
+/* Callback used to write a commit to out. */
+typedef void (*WriteCommitRow)(FILE* out, const CommitInfo* ci);
+
+/* Allocates a cache.
+ *
+ * If an existing file exists at cache_path, it is opened for reading and the
+ * OID header is read.
+ *
+ * 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 WriteCommitRow parameter is a callback that is passed the output
+ * stream of the cache and a CommitInfo struct, and writes formatted output to
+ * the stream. This callback function should emit the same content emitted as
+ * 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);
+
+/* Frees the specified cache. */
+void cache_free(Cache* cache);
+
+/* Returns true if commits can be added to the cache.
+ *
+ * All commits newer than the OID read from the cache at creation time will be
+ * added. Once cache_add_commit_row is invoked with the previously cached OID,
+ * this function will return false.
+ */
+bool cache_can_add_commits(const Cache* cache);
+
+/* Writes a commit row for the specified OID to cache.
+ *
+ * If this is the first commit to be written, the OID will be written to the
+ * first line of the cache before the commit content is written.
+ */
+void cache_add_commit_row(Cache* cache, const CommitInfo* ci);
+
+/* Writes the cache to cache_path, overwriting any existing file.
+ */
+void cache_write(Cache* cache);
+
+/* Copies the contents of the cache to the specified output stream. */
+void cache_copy_log(Cache* cache, FILE* out);
+
+#endif // GITOUT_WRITER_CACHE_CACHE_H_
diff --git a/writer/html/BUILD.gn b/writer/html/BUILD.gn
@@ -0,0 +1,41 @@
+source_set("index_writer") {
+ sources = [
+ "index_writer.c",
+ "index_writer.h",
+ "repo_index.c",
+ "repo_index.h",
+ ]
+ configs += [ "//:gitout_config" ]
+ deps = [
+ "//:format",
+ "//:utils",
+ ]
+ public_deps = [ "//git" ]
+}
+
+source_set("repo_writer") {
+ sources = [
+ "commit.c",
+ "commit.h",
+ "fileblob.c",
+ "fileblob.h",
+ "files.c",
+ "files.h",
+ "log.c",
+ "log.h",
+ "page.c",
+ "page.h",
+ "refs.c",
+ "refs.h",
+ "repo_writer.c",
+ "repo_writer.h",
+ ]
+ configs += [ "//:gitout_config" ]
+ deps = [
+ "//:format",
+ "//:utils",
+ "//writer/atom",
+ "//writer/cache",
+ ]
+ public_deps = [ "//git" ]
+}
diff --git a/writer/html/commit.c b/writer/html/commit.c
@@ -0,0 +1,232 @@
+#include "writer/html/commit.h"
+
+#include <err.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "format.h"
+#include "git/delta.h"
+#include "utils.h"
+#include "writer/html/page.h"
+
+struct Commit {
+ FILE* out;
+ Page* page;
+};
+
+static void commit_write_summary(Commit* commit, const CommitInfo* ci);
+static void commit_write_diffstat(Commit* commit, const CommitInfo* ci);
+static void commit_write_diffstat_row(Commit* commit,
+ size_t row,
+ const DeltaInfo* delta);
+static void commit_write_diff_content(Commit* commit, const CommitInfo* ci);
+static void commit_write_diff_delta(Commit* commit,
+ size_t file_num,
+ const DeltaInfo* delta);
+static void commit_write_diff_hunk(Commit* commit,
+ size_t file_num,
+ const Hunk* hunk);
+
+Commit* commit_create(const RepoInfo* repo,
+ const char* oid,
+ const char* title) {
+ Commit* commit = ecalloc(1, sizeof(Commit));
+ char filename[PATH_MAX];
+ if (snprintf(filename, sizeof(filename), "%s.html", oid) < 0) {
+ err(1, "snprintf");
+ }
+ char path[PATH_MAX];
+ path_concat(path, sizeof(path), "commit", filename);
+ commit->out = efopen(path, "w");
+ commit->page = page_create(commit->out, repo, title, "../");
+ return commit;
+}
+
+void commit_free(Commit* commit) {
+ if (!commit) {
+ return;
+ }
+ fclose(commit->out);
+ commit->out = NULL;
+ page_free(commit->page);
+ commit->page = NULL;
+ free(commit);
+}
+
+void commit_begin(Commit* commit) {
+ page_begin(commit->page);
+}
+
+void commit_add_commit(Commit* commit, const CommitInfo* ci) {
+ static const uint64_t kDiffMaxFiles = 1000;
+ static const uint64_t kDiffMaxDeltas = 1000;
+ static const uint64_t kDiffMaxDeltaLines = 100000;
+
+ commit_write_summary(commit, ci);
+
+ if (ci->deltas_len == 0) {
+ return;
+ }
+ if (ci->filecount > kDiffMaxFiles || ci->deltas_len > kDiffMaxDeltas ||
+ ci->addcount > kDiffMaxDeltaLines || ci->delcount > kDiffMaxDeltaLines) {
+ fprintf(commit->out, "<pre>Diff is too large, output suppressed.</pre>\n");
+ return;
+ }
+
+ commit_write_diffstat(commit, ci);
+ commit_write_diff_content(commit, ci);
+}
+
+void commit_end(Commit* commit) {
+ page_end(commit->page);
+}
+
+void commit_write_summary(Commit* commit, const CommitInfo* ci) {
+ FILE* out = commit->out;
+ fprintf(out, "<pre><b>commit</b> ");
+ fprintf(out, "<a href=\"../commit/%s.html\">%s</a>\n", ci->oid, ci->oid);
+
+ if (ci->parentoid[0] != '\0') {
+ const char* oid = ci->parentoid;
+ fprintf(out, "<b>parent</b> ");
+ fprintf(out, "<a href=\"../commit/%s.html\">%s</a>\n", oid, oid);
+ }
+
+ fprintf(out, "<b>Author:</b> ");
+ print_xml_encoded(commit->out, ci->author_name);
+ fprintf(out, " <<a href=\"mailto:");
+ print_xml_encoded(commit->out, ci->author_email);
+ fprintf(out, "\">");
+ print_xml_encoded(commit->out, ci->author_email);
+ fprintf(out, "</a>>\n");
+ fprintf(out, "<b>Date:</b> ");
+ print_time(out, ci->author_time, ci->author_timezone_offset);
+ fprintf(out, "\n");
+
+ if (ci->msg) {
+ fprintf(out, "\n");
+ print_xml_encoded(commit->out, ci->msg);
+ fprintf(out, "\n");
+ }
+}
+
+void commit_write_diffstat(Commit* commit, const CommitInfo* ci) {
+ fprintf(commit->out, "<b>Diffstat:</b>\n<table>");
+ for (size_t i = 0; i < ci->deltas_len; i++) {
+ commit_write_diffstat_row(commit, i, ci->deltas[i]);
+ }
+ fprintf(commit->out, "</table></pre>");
+}
+
+void commit_write_diffstat_row(Commit* commit,
+ size_t row,
+ const DeltaInfo* delta) {
+ static const size_t kGraphWidth = 78;
+
+ if (delta->status == ' ') {
+ fprintf(commit->out, "<tr><td>");
+ } else {
+ char c = delta->status;
+ fprintf(commit->out, "<tr><td class=\"%c\">%c", c, c);
+ }
+ fprintf(commit->out, "</td>");
+ fprintf(commit->out, "<td>");
+ fprintf(commit->out, "<a href=\"#h%zu\">", row);
+ print_xml_encoded(commit->out, delta->old_file_path);
+ if (strcmp(delta->old_file_path, delta->new_file_path) != 0) {
+ fprintf(commit->out, " -> ");
+ print_xml_encoded(commit->out, delta->new_file_path);
+ }
+ fprintf(commit->out, "</a></td>");
+
+ size_t changed = delta->addcount + delta->delcount;
+ fprintf(commit->out, "<td> | </td>");
+ fprintf(commit->out, "<td class=\"num\">%zu</td>", changed);
+ char* added_graph = deltainfo_added_graph(delta, kGraphWidth);
+ fprintf(commit->out, "<td><span class=\"i\">%s</span>", added_graph);
+ free(added_graph);
+ char* deleted_graph = deltainfo_deleted_graph(delta, kGraphWidth);
+ fprintf(commit->out, "<span class=\"d\">%s</span>", deleted_graph);
+ free(deleted_graph);
+ fprintf(commit->out, "</td></tr>\n");
+}
+
+void commit_write_diff_content(Commit* commit, const CommitInfo* ci) {
+ FILE* out = commit->out;
+ fprintf(out, "<pre>");
+ fprintf(out, "%zu file%s changed, %zu insertion%s(+), %zu deletion%s(-)\n",
+ ci->filecount, ci->filecount == 1 ? "" : "s", //
+ ci->addcount, ci->addcount == 1 ? "" : "s", //
+ ci->delcount, ci->delcount == 1 ? "" : "s");
+ fprintf(commit->out, "<hr/>");
+
+ for (size_t i = 0; i < ci->deltas_len; i++) {
+ commit_write_diff_delta(commit, i, ci->deltas[i]);
+ }
+ fprintf(commit->out, "</pre>\n");
+}
+
+void commit_write_diff_delta(Commit* commit,
+ size_t file_num,
+ const DeltaInfo* delta) {
+ fprintf(commit->out, "<b>diff --git ");
+ fprintf(commit->out, "a/<a id=\"h%zu\" href=\"../file/", file_num);
+ print_percent_encoded(commit->out, delta->old_file_path);
+ fprintf(commit->out, ".html\">");
+ print_xml_encoded(commit->out, delta->old_file_path);
+ fprintf(commit->out, "</a> b/<a href=\"../file/");
+ print_percent_encoded(commit->out, delta->new_file_path);
+ fprintf(commit->out, ".html\">");
+ print_xml_encoded(commit->out, delta->new_file_path);
+ fprintf(commit->out, "</a></b>\n");
+
+ if (delta->is_binary) {
+ fprintf(commit->out, "Binary files differ.\n");
+ } else {
+ for (size_t i = 0; i < delta->hunks_len; i++) {
+ commit_write_diff_hunk(commit, file_num, delta->hunks[i]);
+ }
+ }
+}
+
+void commit_write_diff_hunk(Commit* commit, size_t file_num, const Hunk* hunk) {
+ FILE* out = commit->out;
+
+ // Output header. e.g. @@ -0,0 +1,3 @@
+ char hdr_id[32];
+ if (snprintf(hdr_id, sizeof(hdr_id), "h%zu-%zu", file_num, hunk->id) < 0) {
+ err(1, "snprintf");
+ }
+ fprintf(out, "<a href=\"#%s\" id=\"%s\" class=\"h\">", hdr_id, hdr_id);
+ print_xml_encoded(out, hunk->header);
+ fprintf(out, "</a>");
+
+ // Iterate over lines in hunk.
+ for (size_t i = 0; i < hunk->lines_len; i++) {
+ const HunkLine* line = hunk->lines[i];
+ char line_id[64];
+ if (snprintf(line_id, sizeof(line_id), "%s-%zu", hdr_id, line->id) < 0) {
+ err(1, "snprintf");
+ }
+ if (line->old_lineno == -1) {
+ // Added line. Prefix with +.
+ fprintf(out, "<a href=\"#%s\" id=\"%s\" class=\"i\">+", line_id, line_id);
+ print_xml_encoded_len(out, line->content, line->content_len, false);
+ fprintf(out, "\n</a>");
+ } else if (line->new_lineno == -1) {
+ // Removed line. Prefix with -.
+ fprintf(out, "<a href=\"#%s\" id=\"%s\" class=\"d\">-", line_id, line_id);
+ print_xml_encoded_len(out, line->content, line->content_len, false);
+ fprintf(out, "\n</a>");
+ } else {
+ // Unchanged line. Prefix with ' '. No link.
+ fprintf(out, " ");
+ print_xml_encoded_len(out, line->content, line->content_len, false);
+ fprintf(out, "\n");
+ }
+ }
+}
diff --git a/writer/html/commit.h b/writer/html/commit.h
@@ -0,0 +1,15 @@
+#ifndef GITOUT_WRITER_HTML_COMMIT_H_
+#define GITOUT_WRITER_HTML_COMMIT_H_
+
+#include "git/commit.h"
+#include "git/repo.h"
+
+typedef struct Commit Commit;
+
+Commit* commit_create(const RepoInfo* repo, const char* oid, const char* title);
+void commit_free(Commit* commit);
+void commit_begin(Commit* commit);
+void commit_add_commit(Commit* commit, const CommitInfo* ci);
+void commit_end(Commit* commit);
+
+#endif // GITOUT_WRITER_HTML_COMMIT_H_
diff --git a/writer/html/fileblob.c b/writer/html/fileblob.c
@@ -0,0 +1,115 @@
+#include "writer/html/fileblob.h"
+
+#include <err.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "format.h"
+#include "utils.h"
+#include "writer/html/page.h"
+
+struct FileBlob {
+ const RepoInfo* repo;
+ FILE* out;
+ Page* page;
+};
+
+FileBlob* fileblob_create(const RepoInfo* repo, const char* path) {
+ FileBlob* blob = ecalloc(1, sizeof(FileBlob));
+ blob->repo = repo;
+
+ // Create directories.
+ char filename[PATH_MAX];
+ if (snprintf(filename, sizeof(filename), "%s.html", path) < 0) {
+ err(1, "snprintf");
+ }
+ char out_path[PATH_MAX];
+ path_concat(out_path, sizeof(out_path), "file", filename);
+ char dir[PATH_MAX];
+ estrlcpy(dir, out_path, sizeof(dir));
+ const char* d = dirname(dir);
+ if (!d) {
+ err(1, "dirname");
+ }
+ mkdirp(d);
+ blob->out = efopen(out_path, "w");
+
+ // Compute the relative path.
+ char relpath[PATH_MAX];
+ estrlcpy(relpath, "../", sizeof(relpath));
+ for (const char* p = d; *p != '\0'; p++) {
+ if (*p == '/') {
+ estrlcat(relpath, "../", sizeof(relpath));
+ }
+ }
+ estrlcpy(filename, path, sizeof(filename));
+ const char* title = basename(filename);
+ if (!title) {
+ err(1, "basename");
+ }
+ blob->page = page_create(blob->out, repo, title, relpath);
+ return blob;
+}
+
+void fileblob_free(FileBlob* blob) {
+ if (!blob) {
+ return;
+ }
+ fclose(blob->out);
+ blob->out = NULL;
+ page_free(blob->page);
+ blob->page = NULL;
+ free(blob);
+}
+
+void fileblob_begin(FileBlob* blob) {
+ page_begin(blob->page);
+}
+
+void fileblob_add_file(FileBlob* blob, const FileInfo* fi) {
+ FILE* out = blob->out;
+
+ fprintf(out, "<p> ");
+ char path[PATH_MAX];
+ estrlcpy(path, fi->repo_path, sizeof(path));
+ const char* filename = basename(path);
+ if (!filename) {
+ err(1, "basename");
+ }
+ print_xml_encoded(out, filename);
+ fprintf(out, " (%zdB)", fi->size_bytes);
+ fprintf(out, "</p><hr/>");
+
+ if (fi->size_lines < 0) {
+ fprintf(out, "<p>Binary file.</p>\n");
+ return;
+ }
+
+ fprintf(out, "<pre id=\"blob\">\n");
+
+ size_t i = 0;
+ const char* content = fi->content;
+ const char* end = content + strlen(content);
+ const char* cur_line = content;
+ while (cur_line) {
+ const char* next_line = strchr(cur_line, '\n');
+ if (next_line || cur_line < end) {
+ size_t len = (next_line ? next_line : end) - cur_line;
+ i++;
+ fprintf(out, "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">", i, i);
+ fprintf(out, "%7zu</a> ", i);
+ print_xml_encoded_len(out, cur_line, len, false);
+ fprintf(out, "\n");
+ }
+ cur_line = next_line ? next_line + 1 : NULL;
+ }
+ fprintf(out, "</pre>\n");
+}
+
+void fileblob_end(FileBlob* blob) {
+ page_end(blob->page);
+}
diff --git a/writer/html/fileblob.h b/writer/html/fileblob.h
@@ -0,0 +1,15 @@
+#ifndef GITOUT_WRITER_HTML_FILEBLOB_H_
+#define GITOUT_WRITER_HTML_FILEBLOB_H_
+
+#include "git/file.h"
+#include "git/repo.h"
+
+typedef struct FileBlob FileBlob;
+
+FileBlob* fileblob_create(const RepoInfo* repo, const char* path);
+void fileblob_free(FileBlob* blob);
+void fileblob_begin(FileBlob* blob);
+void fileblob_add_file(FileBlob* blob, const FileInfo* fi);
+void fileblob_end(FileBlob* blob);
+
+#endif // GITOUT_WRITER_HTML_FILEBLOB_H_
diff --git a/writer/html/files.c b/writer/html/files.c
@@ -0,0 +1,69 @@
+#include "writer/html/files.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "format.h"
+#include "utils.h"
+#include "writer/html/page.h"
+
+struct Files {
+ const RepoInfo* repo;
+ FILE* out;
+ Page* page;
+};
+
+Files* files_create(const RepoInfo* repo) {
+ Files* files = ecalloc(1, sizeof(Files));
+ files->repo = repo;
+ files->out = efopen("files.html", "w");
+ files->page = page_create(files->out, repo, "Files", "");
+ return files;
+}
+
+void files_free(Files* files) {
+ if (!files) {
+ return;
+ }
+ fclose(files->out);
+ files->out = NULL;
+ page_free(files->page);
+ files->page = NULL;
+ free(files);
+}
+
+void files_begin(Files* files) {
+ page_begin(files->page);
+ fprintf(files->out,
+ "<table id=\"files\"><thead>\n"
+ "<tr>"
+ "<td><b>Mode</b></td>"
+ "<td><b>Name</b></td>"
+ "<td class=\"num\" align=\"right\"><b>Size</b></td>"
+ "</tr>\n"
+ "</thead><tbody>\n");
+}
+
+void files_add_file(Files* files, const FileInfo* fi) {
+ fprintf(files->out, "<tr><td>%s</td>", fi->mode);
+ fprintf(files->out, "<td><a href=\"file/");
+ print_percent_encoded(files->out, fi->repo_path);
+ fprintf(files->out, ".html\">");
+ print_xml_encoded(files->out, fi->display_path);
+ fprintf(files->out, "</a>");
+ if (fi->commit_oid[0] != '\0') {
+ fprintf(files->out, " @ %s", fi->commit_oid);
+ }
+ fprintf(files->out, "</td><td class=\"num\" align=\"right\">");
+ if (fi->size_lines >= 0) {
+ fprintf(files->out, "%zdL", fi->size_lines);
+ } else if (fi->size_bytes >= 0) {
+ fprintf(files->out, "%zdB", fi->size_bytes);
+ }
+ fprintf(files->out, "</td></tr>\n");
+}
+
+void files_end(Files* files) {
+ fprintf(files->out, "</tbody></table>");
+ page_end(files->page);
+}
diff --git a/writer/html/files.h b/writer/html/files.h
@@ -0,0 +1,15 @@
+#ifndef GITOUT_WRITER_HTML_FILES_H_
+#define GITOUT_WRITER_HTML_FILES_H_
+
+#include "git/file.h"
+#include "git/repo.h"
+
+typedef struct Files Files;
+
+Files* files_create(const RepoInfo* repo);
+void files_free(Files* files);
+void files_begin(Files* files);
+void files_add_file(Files* files, const FileInfo* fi);
+void files_end(Files* files);
+
+#endif // GITOUT_WRITER_HTML_FILES_H_
diff --git a/writer/html/index_writer.c b/writer/html/index_writer.c
@@ -0,0 +1,43 @@
+#include "writer/html/index_writer.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "git/repo.h"
+#include "utils.h"
+#include "writer/html/repo_index.h"
+
+struct HtmlIndexWriter {
+ HtmlRepoIndex* index_;
+};
+
+HtmlIndexWriter* html_indexwriter_create() {
+ HtmlIndexWriter* writer = ecalloc(1, sizeof(HtmlIndexWriter));
+ FILE* out = stdout;
+ writer->index_ = html_repoindex_create(out);
+ return writer;
+}
+
+void html_indexwriter_free(HtmlIndexWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ html_repoindex_free(writer->index_);
+ writer->index_ = NULL;
+ free(writer);
+}
+
+void html_indexwriter_begin(void* writer) {
+ HtmlIndexWriter* html_writer = (HtmlIndexWriter*)writer;
+ html_repoindex_begin(html_writer->index_);
+}
+
+void html_indexwriter_add_repo(void* writer, RepoInfo* ri) {
+ HtmlIndexWriter* html_writer = (HtmlIndexWriter*)writer;
+ html_repoindex_add_repo(html_writer->index_, ri);
+}
+
+void html_indexwriter_end(void* writer) {
+ HtmlIndexWriter* html_writer = (HtmlIndexWriter*)writer;
+ html_repoindex_end(html_writer->index_);
+}
diff --git a/writer/html/index_writer.h b/writer/html/index_writer.h
@@ -0,0 +1,14 @@
+#ifndef GITOUT_WRITER_HTML_INDEX_WRITER_H_
+#define GITOUT_WRITER_HTML_INDEX_WRITER_H_
+
+#include "git/repo.h"
+
+typedef struct HtmlIndexWriter HtmlIndexWriter;
+
+HtmlIndexWriter* html_indexwriter_create();
+void html_indexwriter_free(HtmlIndexWriter* writer);
+void html_indexwriter_begin(void* writer);
+void html_indexwriter_add_repo(void* writer, RepoInfo* ri);
+void html_indexwriter_end(void* writer);
+
+#endif // GITOUT_WRITER_HTML_INDEX_WRITER_H_
diff --git a/writer/html/log.c b/writer/html/log.c
@@ -0,0 +1,115 @@
+#include "writer/html/log.h"
+
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "format.h"
+#include "utils.h"
+#include "writer/cache/cache.h"
+#include "writer/html/page.h"
+
+struct Log {
+ const RepoInfo* repo;
+ FILE* out;
+ Cache* cache;
+ Page* page;
+ size_t remaining_commits;
+ size_t unlogged_commits;
+};
+
+static void write_commit_row(FILE* out, const CommitInfo* ci);
+
+Log* log_create(const RepoInfo* repo) {
+ Log* log = ecalloc(1, sizeof(Log));
+ log->repo = repo;
+ log->out = efopen("log.html", "w");
+ log->page = page_create(log->out, repo, "Log", "");
+ log->remaining_commits = SIZE_MAX;
+ log->unlogged_commits = 0;
+ return log;
+}
+
+void log_free(Log* log) {
+ if (!log) {
+ return;
+ }
+ fclose(log->out);
+ log->out = NULL;
+ cache_free(log->cache);
+ log->cache = NULL;
+ page_free(log->page);
+ log->page = NULL;
+ free(log);
+}
+
+void log_set_cachefile(Log* log, const char* cachefile) {
+ log->cache = cache_create(cachefile, write_commit_row);
+}
+
+void log_set_commit_limit(Log* log, size_t count) {
+ log->remaining_commits = count;
+}
+
+bool log_can_add_commits(const Log* log) {
+ return !log->cache || cache_can_add_commits(log->cache);
+}
+
+void log_begin(Log* log) {
+ page_begin(log->page);
+ fprintf(log->out,
+ "<table id=\"log\"><thead>\n"
+ "<tr>"
+ "<td><b>Date</b></td>"
+ "<td><b>Commit message</b></td>"
+ "<td><b>Author</b></td>"
+ "<td class=\"num\" align=\"right\"><b>Files</b></td>"
+ "<td class=\"num\" align=\"right\"><b>+</b></td>"
+ "<td class=\"num\" align=\"right\"><b>-</b></td>"
+ "</tr>\n"
+ "</thead><tbody>\n");
+}
+
+void log_add_commit(Log* log, const CommitInfo* ci) {
+ if (log->cache) {
+ cache_add_commit_row(log->cache, ci);
+ } else if (log->remaining_commits > 0) {
+ write_commit_row(log->out, ci);
+ log->remaining_commits--;
+ } else {
+ log->unlogged_commits++;
+ }
+}
+
+void log_end(Log* log) {
+ FILE* out = log->out;
+ if (log->cache) {
+ cache_write(log->cache);
+ cache_copy_log(log->cache, log->out);
+ } else if (log->unlogged_commits > 0) {
+ size_t count = log->unlogged_commits;
+ fprintf(out, "<tr><td></td><td colspan=\"5\">");
+ fprintf(out, "%zu more commits remaining, fetch the repository", count);
+ fprintf(out, "</td></tr>\n");
+ }
+ fprintf(out, "</tbody></table>");
+ page_end(log->page);
+}
+
+void write_commit_row(FILE* out, const CommitInfo* ci) {
+ fprintf(out, "<tr><td>");
+ print_time_short(out, ci->author_time);
+ fprintf(out, "</td><td>");
+ if (ci->summary) {
+ fprintf(out, "<a href=\"commit/%s.html\">", ci->oid);
+ print_xml_encoded(out, ci->summary);
+ fprintf(out, "</a>");
+ }
+ fprintf(out, "</td><td>");
+ print_xml_encoded(out, ci->author_name);
+ fprintf(out,
+ "</td>"
+ "<td class=\"num\" align=\"right\">%zu</td>"
+ "<td class=\"num\" align=\"right\">+%zu</td>"
+ "<td class=\"num\" align=\"right\">-%zu</td></tr>\n",
+ ci->filecount, ci->addcount, ci->delcount);
+}
diff --git a/writer/html/log.h b/writer/html/log.h
@@ -0,0 +1,21 @@
+#ifndef GITOUT_WRITER_HTML_LOG_H_
+#define GITOUT_WRITER_HTML_LOG_H_
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "git/commit.h"
+#include "git/repo.h"
+
+typedef struct Log Log;
+
+Log* log_create(const RepoInfo* repo);
+void log_free(Log* log);
+void log_set_cachefile(Log* log, const char* cachefile);
+void log_set_commit_limit(Log* log, size_t count);
+bool log_can_add_commits(const Log* log);
+void log_begin(Log* log);
+void log_add_commit(Log* log, const CommitInfo* commit);
+void log_end(Log* log);
+
+#endif // GITOUT_WRITER_HTML_LOG_H_
diff --git a/writer/html/page.c b/writer/html/page.c
@@ -0,0 +1,119 @@
+#include "writer/html/page.h"
+
+#include <stdlib.h>
+
+#include "format.h"
+#include "utils.h"
+
+struct Page {
+ FILE* out;
+ const RepoInfo* repo;
+ char* title;
+ char* relpath;
+};
+
+Page* page_create(FILE* out,
+ const RepoInfo* repo,
+ const char* title,
+ const char* relpath) {
+ Page* page = ecalloc(1, sizeof(Page));
+ page->out = out;
+ page->repo = repo;
+ page->title = estrdup(title);
+ page->relpath = estrdup(relpath);
+ return page;
+}
+
+void page_free(Page* page) {
+ if (!page) {
+ return;
+ }
+ free(page->title);
+ page->title = NULL;
+ free(page->relpath);
+ page->relpath = NULL;
+ free(page);
+}
+
+void page_begin(Page* page) {
+ FILE* out = page->out;
+ fprintf(
+ out,
+ "<!DOCTYPE html>\n"
+ "<html>\n"
+ "<head>\n"
+ "<meta "
+ "http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
+ "<meta "
+ "name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n");
+ fprintf(out, "<title>");
+
+ print_xml_encoded(out, page->title);
+ if (page->title[0] != '\0' && page->repo->short_name[0] != '\0') {
+ fprintf(out, " - ");
+ print_xml_encoded(out, page->repo->short_name);
+ }
+ if (page->title[0] != '\0' && page->repo->description[0] != '\0') {
+ fprintf(out, " - ");
+ print_xml_encoded(out, page->repo->description);
+ }
+
+ const char* relpath = page->relpath;
+ fprintf(out, "</title>\n");
+ fprintf(out,
+ "<link rel=\"icon\" type=\"image/png\" href=\"%sfavicon.png\" />\n",
+ relpath);
+ fprintf(out,
+ "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"");
+ 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, page->repo->name);
+ fprintf(out, " Atom Feed (tags)\" href=\"%stags.xml\" />\n", relpath);
+ fprintf(out,
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"%sstyle.css\" />",
+ relpath);
+ fprintf(out, "\n</head>\n<body>\n<table><tr><td>");
+ fprintf(out, "<a href=\"../%s\">", relpath);
+ fprintf(out, "<img src=\"%slogo.png\" alt=\"\" width=\"32\" height=\"32\" />",
+ relpath);
+ fprintf(out, "</a></td><td><h1>");
+ print_xml_encoded(out, page->repo->short_name);
+ fprintf(out, "</h1><span class=\"desc\">");
+ print_xml_encoded(out, page->repo->description);
+ fprintf(out, "</span></td></tr>");
+
+ if (page->repo->clone_url[0] != '\0') {
+ fprintf(out, "<tr class=\"url\"><td></td><td>git clone <a href=\"");
+ print_xml_encoded(out, page->repo->clone_url);
+ fprintf(out, "\">");
+ print_xml_encoded(out, page->repo->clone_url);
+ fprintf(out, "</a></td></tr>");
+ }
+ fprintf(out, "<tr><td></td><td>\n");
+ fprintf(out, "<a href=\"%slog.html\">Log</a> | ", relpath);
+ fprintf(out, "<a href=\"%sfiles.html\">Files</a> | ", relpath);
+ fprintf(out, "<a href=\"%srefs.html\">Refs</a>", relpath);
+
+ if (page->repo->submodules[0] != '\0') {
+ fprintf(out, " | <a href=\"%sfile/", relpath);
+ print_xml_encoded(out, page->repo->submodules);
+ fprintf(out, ".html\">Submodules</a>");
+ }
+ if (page->repo->readme[0] != '\0') {
+ fprintf(out, " | <a href=\"%sfile/", relpath);
+ print_xml_encoded(out, page->repo->readme);
+ fprintf(out, ".html\">README</a>");
+ }
+ if (page->repo->license[0] != '\0') {
+ fprintf(out, " | <a href=\"%sfile/", relpath);
+ print_xml_encoded(out, page->repo->license);
+ fprintf(out, ".html\">LICENSE</a>");
+ }
+ fprintf(out, "</td></tr></table>\n<hr/>\n<div id=\"content\">\n");
+}
+
+void page_end(Page* page) {
+ fprintf(page->out, "</div>\n</body>\n</html>\n");
+}
diff --git a/writer/html/page.h b/writer/html/page.h
@@ -0,0 +1,18 @@
+#ifndef GITOUT_WRITER_HTML_PAGE_H_
+#define GITOUT_WRITER_HTML_PAGE_H_
+
+#include <stdio.h>
+
+#include "git/repo.h"
+
+typedef struct Page Page;
+
+Page* page_create(FILE* out,
+ const RepoInfo* repo,
+ const char* title,
+ const char* relpath);
+void page_free(Page* page);
+void page_begin(Page* page);
+void page_end(Page* page);
+
+#endif // GITOUT_WRITER_HTML_PAGE_H_
diff --git a/writer/html/refs.c b/writer/html/refs.c
@@ -0,0 +1,143 @@
+#include "writer/html/refs.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "format.h"
+#include "git/commit.h"
+#include "utils.h"
+#include "writer/html/page.h"
+
+typedef struct {
+ char* title;
+ char* id;
+ FILE* out;
+} RefsTable;
+
+struct Refs {
+ const RepoInfo* repo;
+ FILE* out;
+ Page* page;
+ RefsTable* branches;
+ RefsTable* tags;
+};
+
+static RefsTable* refstable_create(const char* title,
+ const char* id,
+ FILE* out);
+static void refstable_free(RefsTable* table);
+static void refstable_begin(RefsTable* table);
+static void refstable_add_ref(RefsTable* table, const ReferenceInfo* ri);
+static void refstable_end(RefsTable* table);
+
+RefsTable* refstable_create(const char* title, const char* id, FILE* out) {
+ RefsTable* table = ecalloc(1, sizeof(RefsTable));
+ table->title = estrdup(title);
+ table->id = estrdup(id);
+ table->out = out;
+ return table;
+}
+
+void refstable_free(RefsTable* table) {
+ if (!table) {
+ return;
+ }
+ free(table->title);
+ table->title = NULL;
+ free(table->id);
+ table->id = NULL;
+ free(table);
+}
+
+void refstable_begin(RefsTable* table) {
+ fprintf(table->out, "<h2>%s</h2>", table->title);
+ fprintf(table->out, "<table id=\"%s\">", table->id);
+ fprintf(table->out,
+ "<thead>\n"
+ "<tr>"
+ "<td><b>Name</b></td>"
+ "<td><b>Last commit date</b></td>"
+ "<td><b>Author</b></td>\n"
+ "</tr>\n"
+ "</thead><tbody>\n");
+}
+
+void refstable_add_ref(RefsTable* table, const ReferenceInfo* ri) {
+ CommitInfo* ci = ri->ci;
+ fprintf(table->out, "<tr><td>");
+ print_xml_encoded(table->out, ri->shorthand);
+ fprintf(table->out, "</td><td>");
+ print_time_short(table->out, ci->author_time);
+ fprintf(table->out, "</td><td>");
+ print_xml_encoded(table->out, ci->author_name);
+ fprintf(table->out, "</td></tr>\n");
+}
+
+void refstable_end(RefsTable* table) {
+ fprintf(table->out, "</tbody></table><br/>\n");
+}
+
+Refs* refs_create(const RepoInfo* repo) {
+ Refs* refs = ecalloc(1, sizeof(Refs));
+ refs->repo = repo;
+ refs->out = efopen("refs.html", "w");
+ refs->page = page_create(refs->out, repo, "Refs", "");
+ return refs;
+}
+
+void refs_free(Refs* refs) {
+ if (!refs) {
+ return;
+ }
+ fclose(refs->out);
+ refs->out = NULL;
+ page_free(refs->page);
+ refs->page = NULL;
+ refstable_free(refs->branches);
+ refs->branches = NULL;
+ refstable_free(refs->tags);
+ refs->tags = NULL;
+ free(refs);
+}
+
+void refs_begin(Refs* refs) {
+ page_begin(refs->page);
+}
+
+void refs_add_ref(Refs* refs, const ReferenceInfo* ri) {
+ switch (ri->type) {
+ case kReftypeBranch:
+ if (!refs->branches) {
+ refs->branches = refstable_create("Branches", "branches", refs->out);
+ refstable_begin(refs->branches);
+ }
+ refstable_add_ref(refs->branches, ri);
+ break;
+ case kReftypeTag:
+ if (refs->branches) {
+ refstable_end(refs->branches);
+ refstable_free(refs->branches);
+ refs->branches = NULL;
+ }
+ if (!refs->tags) {
+ refs->tags = refstable_create("Tags", "tags", refs->out);
+ refstable_begin(refs->tags);
+ }
+ refstable_add_ref(refs->tags, ri);
+ break;
+ }
+}
+
+void refs_end(Refs* refs) {
+ if (refs->branches) {
+ refstable_end(refs->branches);
+ refstable_free(refs->branches);
+ refs->branches = NULL;
+ }
+ if (refs->tags) {
+ refstable_end(refs->tags);
+ refstable_free(refs->tags);
+ refs->tags = NULL;
+ }
+ page_end(refs->page);
+}
diff --git a/writer/html/refs.h b/writer/html/refs.h
@@ -0,0 +1,15 @@
+#ifndef GITOUT_WRITER_HTML_REFS_H_
+#define GITOUT_WRITER_HTML_REFS_H_
+
+#include "git/reference.h"
+#include "git/repo.h"
+
+typedef struct Refs Refs;
+
+Refs* refs_create(const RepoInfo* repo);
+void refs_free(Refs* refs);
+void refs_begin(Refs* refs);
+void refs_add_ref(Refs* refs, const ReferenceInfo* ri);
+void refs_end(Refs* refs);
+
+#endif // GITOUT_WRITER_HTML_REFS_H_
diff --git a/writer/html/repo_index.c b/writer/html/repo_index.c
@@ -0,0 +1,79 @@
+#include "writer/html/repo_index.h"
+
+#include <stdlib.h>
+
+#include "format.h"
+#include "git/commit.h"
+#include "utils.h"
+
+struct HtmlRepoIndex {
+ FILE* out;
+};
+
+HtmlRepoIndex* html_repoindex_create(FILE* out) {
+ HtmlRepoIndex* index = ecalloc(1, sizeof(HtmlRepoIndex));
+ index->out = out;
+ return index;
+}
+
+void html_repoindex_free(HtmlRepoIndex* index) {
+ if (!index) {
+ return;
+ }
+ fclose(index->out);
+ index->out = NULL;
+ free(index);
+}
+
+void html_repoindex_begin(HtmlRepoIndex* index) {
+ FILE* out = index->out;
+ fprintf(
+ out,
+ "<!DOCTYPE html>\n"
+ "<html>\n"
+ "<head>\n"
+ "<meta "
+ "http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
+ "<meta "
+ "name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
+ "<title>Repositories</title>\n"
+ "<link rel=\"icon\" type=\"image/png\" href=\"favicon.png\" />\n"
+ "<link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />\n"
+ "</head>\n"
+ "<body>\n"
+ "<table>\n<tr><td>"
+ "<img src=\"logo.png\" alt=\"\" width=\"32\" height=\"32\" /></td>\n"
+ "<td><span class=\"desc\">Repositories</span></td></tr>"
+ "<tr><td></td><td>\n"
+ "</td></tr>\n"
+ "</table>\n<hr/>\n<div id=\"content\">\n"
+ "<table id=\"index\"><thead>\n"
+ "<tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Owner</b></td>"
+ "<td><b>Last commit</b></td></tr>"
+ "</thead><tbody>\n");
+}
+
+static void print_author_time(const CommitInfo* ci, void* user_data) {
+ FILE* out = (FILE*)user_data;
+ print_time_short(out, ci->author_time);
+}
+
+void html_repoindex_add_repo(HtmlRepoIndex* index, RepoInfo* ri) {
+ FILE* out = index->out;
+ fprintf(out, "<tr><td><a href=\"");
+ print_percent_encoded(out, ri->short_name);
+ fprintf(out, "/log.html\">");
+ print_xml_encoded(out, ri->short_name);
+ fprintf(out, "</a></td><td>");
+ print_xml_encoded(out, ri->description);
+ fprintf(out, "</td><td>");
+ print_xml_encoded(out, ri->owner);
+ fprintf(out, "</td><td>");
+ repoinfo_for_commit(ri, "HEAD", print_author_time, out);
+ fprintf(out, "</td></tr>");
+}
+
+void html_repoindex_end(HtmlRepoIndex* index) {
+ FILE* out = index->out;
+ fprintf(out, "</tbody>\n</table>\n</div>\n</body>\n</html>\n");
+}
diff --git a/writer/html/repo_index.h b/writer/html/repo_index.h
@@ -0,0 +1,16 @@
+#ifndef GITOUT_WRITER_HTML_REPOINDEX_H_
+#define GITOUT_WRITER_HTML_REPOINDEX_H_
+
+#include "git/repo.h"
+
+#include <stdio.h>
+
+typedef struct HtmlRepoIndex HtmlRepoIndex;
+
+HtmlRepoIndex* html_repoindex_create(FILE* out);
+void html_repoindex_free(HtmlRepoIndex* index);
+void html_repoindex_begin(HtmlRepoIndex* index);
+void html_repoindex_add_repo(HtmlRepoIndex* index, RepoInfo* ri);
+void html_repoindex_end(HtmlRepoIndex* index);
+
+#endif // GITOUT_WRITER_HTML_REPOINDEX_H_
diff --git a/writer/html/repo_writer.c b/writer/html/repo_writer.c
@@ -0,0 +1,139 @@
+#include "writer/html/repo_writer.h"
+
+#include <err.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "git/commit.h"
+#include "git/file.h"
+#include "git/reference.h"
+#include "utils.h"
+#include "writer/atom/atom.h"
+#include "writer/html/commit.h"
+#include "writer/html/fileblob.h"
+#include "writer/html/files.h"
+#include "writer/html/log.h"
+#include "writer/html/refs.h"
+
+struct HtmlRepoWriter {
+ const RepoInfo* repo_;
+ Refs* refs_;
+ Log* log_;
+ Atom* atom_;
+ Atom* tags_;
+ Files* files_;
+};
+
+HtmlRepoWriter* html_repo_writer_create(const RepoInfo* repo) {
+ HtmlRepoWriter* writer = ecalloc(1, sizeof(HtmlRepoWriter));
+ writer->repo_ = repo;
+ writer->refs_ = refs_create(repo);
+ writer->log_ = log_create(repo);
+ writer->atom_ = atom_create(repo, kAtomTypeAll);
+ writer->tags_ = atom_create(repo, kAtomTypeTags);
+ writer->files_ = files_create(repo);
+ return writer;
+}
+
+void html_repo_writer_free(HtmlRepoWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ refs_free(writer->refs_);
+ writer->refs_ = NULL;
+ log_free(writer->log_);
+ writer->log_ = NULL;
+ atom_free(writer->atom_);
+ writer->atom_ = NULL;
+ atom_free(writer->tags_);
+ writer->tags_ = NULL;
+ files_free(writer->files_);
+ writer->files_ = NULL;
+ free(writer);
+}
+
+void html_repo_writer_set_log_cachefile(void* writer, const char* cachefile) {
+ HtmlRepoWriter* html_writer = (HtmlRepoWriter*)writer;
+ log_set_cachefile(html_writer->log_, cachefile);
+}
+
+void html_repo_writer_set_log_commit_limit(void* writer, size_t count) {
+ HtmlRepoWriter* html_writer = (HtmlRepoWriter*)writer;
+ log_set_commit_limit(html_writer->log_, count);
+}
+
+void html_repo_writer_set_baseurl(void* writer, const char* baseurl) {
+ HtmlRepoWriter* html_writer = (HtmlRepoWriter*)writer;
+ atom_set_baseurl(html_writer->atom_, baseurl);
+ atom_set_baseurl(html_writer->tags_, baseurl);
+}
+
+void html_repo_writer_begin(void* writer) {
+ HtmlRepoWriter* html_writer = (HtmlRepoWriter*)writer;
+ mkdir("commit", S_IRWXU | S_IRWXG | S_IRWXO);
+ mkdir("file", S_IRWXU | S_IRWXG | S_IRWXO);
+
+ refs_begin(html_writer->refs_);
+ log_begin(html_writer->log_);
+ atom_begin(html_writer->atom_);
+ atom_begin(html_writer->tags_);
+ files_begin(html_writer->files_);
+}
+
+void html_repo_writer_add_commit(void* writer, const CommitInfo* ci) {
+ HtmlRepoWriter* html_writer = (HtmlRepoWriter*)writer;
+ char filename[PATH_MAX];
+ if (snprintf(filename, sizeof(filename), "%s.html", ci->oid) < 0) {
+ err(1, "snprintf");
+ }
+ char path[PATH_MAX];
+ path_concat(path, sizeof(path), "commit", filename);
+ atom_add_commit(html_writer->atom_, ci, path, "text/html", "");
+
+ if (log_can_add_commits(html_writer->log_)) {
+ log_add_commit(html_writer->log_, ci);
+ Commit* commit = commit_create(html_writer->repo_, ci->oid, ci->summary);
+ commit_begin(commit);
+ commit_add_commit(commit, ci);
+ commit_end(commit);
+ commit_free(commit);
+ }
+}
+
+void html_repo_writer_add_reference(void* writer, const ReferenceInfo* ri) {
+ HtmlRepoWriter* html_writer = (HtmlRepoWriter*)writer;
+ refs_add_ref(html_writer->refs_, ri);
+ if (ri->type == kReftypeTag) {
+ char filename[PATH_MAX];
+ if (snprintf(filename, sizeof(filename), "%s.html", ri->ci->oid) < 0) {
+ err(1, "snprintf");
+ }
+ char path[PATH_MAX];
+ path_concat(path, sizeof(path), "commit", filename);
+ atom_add_commit(html_writer->tags_, ri->ci, path, "text/html",
+ ri->shorthand);
+ }
+}
+
+void html_repo_writer_add_file(void* writer, const FileInfo* fi) {
+ HtmlRepoWriter* html_writer = (HtmlRepoWriter*)writer;
+ files_add_file(html_writer->files_, fi);
+
+ FileBlob* blob = fileblob_create(html_writer->repo_, fi->repo_path);
+ fileblob_begin(blob);
+ fileblob_add_file(blob, fi);
+ fileblob_end(blob);
+ fileblob_free(blob);
+}
+
+void html_repo_writer_end(void* writer) {
+ HtmlRepoWriter* html_writer = (HtmlRepoWriter*)writer;
+ refs_end(html_writer->refs_);
+ log_end(html_writer->log_);
+ atom_end(html_writer->atom_);
+ atom_end(html_writer->tags_);
+ files_end(html_writer->files_);
+}
diff --git a/writer/html/repo_writer.h b/writer/html/repo_writer.h
@@ -0,0 +1,24 @@
+#ifndef GITOUT_WRITER_HTML_REPO_WRITER_H_
+#define GITOUT_WRITER_HTML_REPO_WRITER_H_
+
+#include <stddef.h>
+
+#include "git/commit.h"
+#include "git/file.h"
+#include "git/reference.h"
+#include "git/repo.h"
+
+typedef struct HtmlRepoWriter HtmlRepoWriter;
+
+HtmlRepoWriter* html_repo_writer_create(const RepoInfo* repo);
+void html_repo_writer_free(HtmlRepoWriter* writer);
+void html_repo_writer_set_log_cachefile(void* writer, const char* cachefile);
+void html_repo_writer_set_log_commit_limit(void* writer, size_t count);
+void html_repo_writer_set_baseurl(void* writer, const char* baseurl);
+void html_repo_writer_begin(void* writer);
+void html_repo_writer_add_commit(void* writer, const CommitInfo* ci);
+void html_repo_writer_add_reference(void* writer, const ReferenceInfo* ri);
+void html_repo_writer_add_file(void* writer, const FileInfo* fi);
+void html_repo_writer_end(void* writer);
+
+#endif // GITOUT_WRITER_HTML_REPO_WRITER_H_
diff --git a/writer/index_writer.c b/writer/index_writer.c
@@ -0,0 +1,79 @@
+#include "writer/index_writer.h"
+
+#include <err.h>
+#include <stdlib.h>
+
+#include "utils.h"
+#include "writer/html/index_writer.h"
+
+typedef void (*IndexWriterBegin)(void* impl);
+typedef void (*IndexWriterAddRepo)(void* impl, RepoInfo* ri);
+typedef void (*IndexWriterEnd)(void* impl);
+
+struct IndexWriter {
+ /* Writer implementation. */
+ IndexWriterType type;
+ void* impl;
+
+ /* Writer operations. */
+ IndexWriterBegin begin;
+ IndexWriterAddRepo add_repo;
+ IndexWriterEnd end;
+};
+
+static IndexWriter* htmlindexwriter_create();
+static void htmlindexwriter_free(IndexWriter* writer);
+
+IndexWriter* indexwriter_create(IndexWriterType type) {
+ switch (type) {
+ case kIndexWriterTypeHtml:
+ return htmlindexwriter_create();
+ }
+ errx(1, "unknown IndexWriterType %d", type);
+}
+
+void indexwriter_free(IndexWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ switch (writer->type) {
+ case kIndexWriterTypeHtml:
+ htmlindexwriter_free(writer);
+ return;
+ }
+ errx(1, "unknown IndexWriterType %d", writer->type);
+}
+
+void indexwriter_begin(IndexWriter* writer) {
+ writer->begin(writer->impl);
+}
+
+void indexwriter_add_repo(IndexWriter* writer, RepoInfo* ri) {
+ writer->add_repo(writer->impl, ri);
+}
+
+void indexwriter_end(IndexWriter* writer) {
+ writer->end(writer->impl);
+}
+
+/* HtmlIndexWriter setup/teardown. */
+
+IndexWriter* htmlindexwriter_create() {
+ IndexWriter* writer = ecalloc(1, sizeof(IndexWriter));
+ HtmlIndexWriter* html_writer = html_indexwriter_create();
+ writer->type = kIndexWriterTypeHtml;
+ writer->impl = html_writer;
+ writer->begin = html_indexwriter_begin;
+ writer->add_repo = html_indexwriter_add_repo;
+ writer->end = html_indexwriter_end;
+ return writer;
+}
+
+void htmlindexwriter_free(IndexWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ html_indexwriter_free(writer->impl);
+ writer->impl = NULL;
+ free(writer);
+}
diff --git a/writer/index_writer.h b/writer/index_writer.h
@@ -0,0 +1,18 @@
+#ifndef GITOUT_WRITER_INDEX_WRITER_H_
+#define GITOUT_WRITER_INDEX_WRITER_H_
+
+#include "git/repo.h"
+
+typedef enum {
+ kIndexWriterTypeHtml,
+} IndexWriterType;
+
+typedef struct IndexWriter IndexWriter;
+
+IndexWriter* indexwriter_create(IndexWriterType type);
+void indexwriter_free(IndexWriter* writer);
+void indexwriter_begin(IndexWriter* writer);
+void indexwriter_add_repo(IndexWriter* writer, RepoInfo* ri);
+void indexwriter_end(IndexWriter* writer);
+
+#endif // GITOUT_WRITER_INDEX_WRITER_H_
diff --git a/writer/repo_writer.c b/writer/repo_writer.c
@@ -0,0 +1,116 @@
+#include "writer/repo_writer.h"
+
+#include <err.h>
+#include <stdlib.h>
+
+#include "utils.h"
+#include "writer/html/repo_writer.h"
+
+typedef void (*RepoWriterSetLogCachefile)(void* writer, const char* cachefile);
+typedef void (*RepoWriterSetLogCommitLimit)(void* writer, size_t count);
+typedef void (*RepoWriterSetBaseurl)(void* writer, const char* baseurl);
+typedef void (*RepoWriterBegin)(void* writer);
+typedef void (*RepoWriterAddCommit)(void* writer, const CommitInfo* ci);
+typedef void (*RepoWriterAddReference)(void* writer, const ReferenceInfo* ri);
+typedef void (*RepoWriterAddFile)(void* writer, const FileInfo* fi);
+typedef void (*RepoWriterEnd)(void* writer);
+
+struct RepoWriter {
+ /* Writer implementation. */
+ RepoWriterType type;
+ void* impl;
+
+ /* Writer configuration. */
+ RepoWriterSetLogCachefile set_log_cachefile;
+ RepoWriterSetLogCommitLimit set_log_commit_limit;
+ RepoWriterSetBaseurl set_baseurl;
+
+ /* Writer operations. */
+ RepoWriterBegin begin;
+ RepoWriterAddCommit add_commit;
+ RepoWriterAddReference add_reference;
+ RepoWriterAddFile add_file;
+ RepoWriterEnd end;
+};
+
+static RepoWriter* htmlrepowriter_create(RepoInfo* ri);
+static void htmlrepowriter_free(RepoWriter* writer);
+
+RepoWriter* repowriter_create(RepoWriterType type, RepoInfo* ri) {
+ switch (type) {
+ case kRepoWriterTypeHtml:
+ return htmlrepowriter_create(ri);
+ }
+ errx(1, "unknown RepoWriterType %d", type);
+}
+
+void repowriter_free(RepoWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ switch (writer->type) {
+ case kRepoWriterTypeHtml:
+ htmlrepowriter_free(writer);
+ return;
+ }
+ errx(1, "unknown RepoWriterType %d", writer->type);
+}
+
+void repowriter_set_log_cachefile(RepoWriter* writer, const char* cachefile) {
+ writer->set_log_cachefile(writer->impl, cachefile);
+}
+
+void repowriter_set_log_commit_limit(RepoWriter* writer, size_t count) {
+ writer->set_log_commit_limit(writer->impl, count);
+}
+
+void repowriter_set_baseurl(RepoWriter* writer, const char* baseurl) {
+ writer->set_baseurl(writer->impl, baseurl);
+}
+
+void repowriter_begin(RepoWriter* writer) {
+ writer->begin(writer->impl);
+}
+
+void repowriter_add_commit(RepoWriter* writer, const CommitInfo* ci) {
+ writer->add_commit(writer->impl, ci);
+}
+
+void repowriter_add_reference(RepoWriter* writer, const ReferenceInfo* ri) {
+ writer->add_reference(writer->impl, ri);
+}
+
+void repowriter_add_file(RepoWriter* writer, const FileInfo* fi) {
+ writer->add_file(writer->impl, fi);
+}
+
+void repowriter_end(RepoWriter* writer) {
+ writer->end(writer->impl);
+}
+
+/* HtmlRepoWriter setup/teardown. */
+
+RepoWriter* htmlrepowriter_create(RepoInfo* ri) {
+ RepoWriter* writer = ecalloc(1, sizeof(RepoWriter));
+ HtmlRepoWriter* html_writer = html_repo_writer_create(ri);
+ writer->type = kRepoWriterTypeHtml;
+ writer->impl = html_writer;
+ writer->set_log_cachefile = html_repo_writer_set_log_cachefile;
+ writer->set_log_commit_limit = html_repo_writer_set_log_commit_limit;
+ writer->set_baseurl = html_repo_writer_set_baseurl;
+ writer->begin = html_repo_writer_begin;
+ writer->add_commit = html_repo_writer_add_commit;
+ writer->add_reference = html_repo_writer_add_reference;
+ writer->add_file = html_repo_writer_add_file;
+ writer->end = html_repo_writer_end;
+ return writer;
+}
+
+void htmlrepowriter_free(RepoWriter* writer) {
+ if (!writer) {
+ return;
+ }
+ html_repo_writer_free(writer->impl);
+ writer->impl = NULL;
+ free(writer);
+}
diff --git a/writer/repo_writer.h b/writer/repo_writer.h
@@ -0,0 +1,30 @@
+#ifndef GITOUT_WRITER_REPO_WRITER_H_
+#define GITOUT_WRITER_REPO_WRITER_H_
+
+#include <stddef.h>
+
+#include "git/commit.h"
+#include "git/file.h"
+#include "git/reference.h"
+#include "git/repo.h"
+
+typedef enum {
+ kRepoWriterTypeHtml,
+} RepoWriterType;
+
+typedef struct RepoWriter RepoWriter;
+
+RepoWriter* repowriter_create(RepoWriterType type, RepoInfo* ri);
+void repowriter_free(RepoWriter* ri);
+
+void repowriter_set_log_cachefile(RepoWriter* writer, const char* cachefile);
+void repowriter_set_log_commit_limit(RepoWriter* writer, size_t count);
+void repowriter_set_baseurl(RepoWriter* writer, const char* baseurl);
+
+void repowriter_begin(RepoWriter* writer);
+void repowriter_add_commit(RepoWriter* writer, const CommitInfo* ci);
+void repowriter_add_reference(RepoWriter* writer, const ReferenceInfo* ri);
+void repowriter_add_file(RepoWriter* writer, const FileInfo* fi);
+void repowriter_end(RepoWriter* writer);
+
+#endif // GITOUT_WRITER_REPO_WRITER_H_