gitout

A static git page generator
git clone https://git.bracken.jp/gitout.git
Log | Files | Refs | Submodules | README | LICENSE

commit 5bc9a422994e6088c2baaf9c901c345acb11ce17
Author: Chris Bracken <chris@bracken.jp>
Date:   Tue,  2 Jan 2024 22:01:12 +0900

Initial commit

Diffstat:
A.clang-format | 9+++++++++
A.clang-tidy | 43+++++++++++++++++++++++++++++++++++++++++++
A.gitignore | 6++++++
A.gitmodules | 3+++
A.gn | 24++++++++++++++++++++++++
ABUILD.gn | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE | 22++++++++++++++++++++++
AMakefile | 24++++++++++++++++++++++++
AREADME.md | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild/BUILD.gn | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild/BUILDCONFIG.gn | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild/toolchain/BUILD.gn | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aformat.c | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aformat.h | 35+++++++++++++++++++++++++++++++++++
Agit/BUILD.gn | 28++++++++++++++++++++++++++++
Agit/commit.c | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agit/commit.h | 30++++++++++++++++++++++++++++++
Agit/delta.c | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agit/delta.h | 39+++++++++++++++++++++++++++++++++++++++
Agit/file.c | 30++++++++++++++++++++++++++++++
Agit/file.h | 23+++++++++++++++++++++++
Agit/git.c | 24++++++++++++++++++++++++
Agit/git.h | 11+++++++++++
Agit/internal.h | 31+++++++++++++++++++++++++++++++
Agit/reference.c | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agit/reference.h | 21+++++++++++++++++++++
Agit/repo.c | 448+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agit/repo.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Agitout.c | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agitout.h | 12++++++++++++
Agitout_index.c | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agitout_index.h | 12++++++++++++
Agitout_index_main.c | 21+++++++++++++++++++++
Agitout_main.c | 23+++++++++++++++++++++++
Asecondary/third_party/googletest/BUILD.gn | 384+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asecurity.c | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asecurity.h | 25+++++++++++++++++++++++++
Athird_party/googletest | 1+
Athird_party/openbsd/BUILD.gn | 32++++++++++++++++++++++++++++++++
Athird_party/openbsd/reallocarray.c | 37+++++++++++++++++++++++++++++++++++++
Athird_party/openbsd/reallocarray.h | 9+++++++++
Athird_party/openbsd/strlcat.c | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/openbsd/strlcat.h | 9+++++++++
Athird_party/openbsd/strlcpy.c | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/openbsd/strlcpy.h | 9+++++++++
Autils.c | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils.h | 30++++++++++++++++++++++++++++++
Autils_test.cc | 29+++++++++++++++++++++++++++++
Awriter/BUILD.gn | 19+++++++++++++++++++
Awriter/atom/BUILD.gn | 12++++++++++++
Awriter/atom/atom.c | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/atom/atom.h | 40++++++++++++++++++++++++++++++++++++++++
Awriter/cache/BUILD.gn | 9+++++++++
Awriter/cache/cache.c | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/cache/cache.h | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/html/BUILD.gn | 41+++++++++++++++++++++++++++++++++++++++++
Awriter/html/commit.c | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/html/commit.h | 15+++++++++++++++
Awriter/html/fileblob.c | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/html/fileblob.h | 15+++++++++++++++
Awriter/html/files.c | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/html/files.h | 15+++++++++++++++
Awriter/html/index_writer.c | 43+++++++++++++++++++++++++++++++++++++++++++
Awriter/html/index_writer.h | 14++++++++++++++
Awriter/html/log.c | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/html/log.h | 21+++++++++++++++++++++
Awriter/html/page.c | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/html/page.h | 18++++++++++++++++++
Awriter/html/refs.c | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/html/refs.h | 15+++++++++++++++
Awriter/html/repo_index.c | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/html/repo_index.h | 16++++++++++++++++
Awriter/html/repo_writer.c | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/html/repo_writer.h | 24++++++++++++++++++++++++
Awriter/index_writer.c | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/index_writer.h | 18++++++++++++++++++
Awriter/repo_writer.c | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awriter/repo_writer.h | 30++++++++++++++++++++++++++++++
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, "&lt;"); + break; + case '>': + fprintf(out, "&gt;"); + break; + case '\'': + fprintf(out, "&#39;"); + break; + case '&': + fprintf(out, "&amp;"); + break; + case '"': + fprintf(out, "&quot;"); + 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(&current, revwalk)) { + CommitInfo* ci = commitinfo_create(&current, 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(&current, revwalk)) { + CommitInfo* ci = commitinfo_create(&current, 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(&current, 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, " &lt;"); + print_xml_encoded(atom->out, ci->author_email); + fprintf(atom->out, "&gt;\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, " &lt;<a href=\"mailto:"); + print_xml_encoded(commit->out, ci->author_email); + fprintf(out, "\">"); + print_xml_encoded(commit->out, ci->author_email); + fprintf(out, "</a>&gt;\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, " -&gt; "); + 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_