docs generate browsable odoc HTML with dune build @doc

dune build @doc produced an empty package index: odoc skips private libraries, and the ogit library had no public_name. Make it public, which matches the executables that were already public and leaves ogit.opam unchanged. Add doc/index.mld as the package landing page, ordering the modules by the request flow rather than alphabetically, and doc/conventions.mld recording where documentation is expected to live. scripts/docs.sh builds and opens the result, since the entry point is buried in _build. Fill in the module-level documentation that was missing: Config, Diff, Handlers, Main, Routes, Static_handler, both bin executables, and nine test modules. Where behaviour is usable but undescribed, or a rough edge is worth recording, the comment carries a TODO — the hard-coded static cache lifetime, Diff's large-input fallback threshold, and ogit-write-config clobbering its target without warning. Also disambiguate five odoc references in ui.mli that collided with same-named sections or types, so the doc build is warning-free. Verified: dune clean followed by fmt, build, 62 tests and @doc all pass with no warnings; 34 HTML pages generated; every module page except the crunch-generated Static_assets carries a description; all 17 module links on the landing page resolve; server smoke test returns 200 for a page and a static asset.

Commit
2a33ef711455956fa42dd09cf5f0e77b8d41420c
Author
Claude Sonnet 4 <claude@anthropic.invalid>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
README.org
index 15d33e62..5c1a1cc1 100644..100644
@@ -63,6 +63,25 @@
63 63 =Time_fmt= formats Git dates. =Charts= generates standalone SVG.
64 64
65 65
66 Added: * Documentation
67 Added:
68 Added: API documentation is generated with odoc:
69 Added:
70 Added: #+begin_src sh
71 Added: dune build @doc
72 Added: #+end_src
73 Added:
74 Added: The entry point is =_build/default/_doc/_html/index.html=.
75 Added: =scripts/docs.sh= runs the same build and opens that file in a browser;
76 Added: =scripts/docs.sh --print= prints the path instead.
77 Added:
78 Added: The landing page at =doc/index.mld= orders the modules by the request
79 Added: flow described above. =doc/conventions.mld= records where documentation
80 Added: is expected to live.
81 Added:
82 Added: Unresolved type links pointing into Dream, Toml, Git or dream-html are
83 Added: expected: those packages' own documentation is not built here.
84 Added:
66 85 * Configuration
67 86
68 87 Ogit reads the path named by =OGIT_CONFIG=. Otherwise it checks
bin/config_writer.ml
index c70e2aae..a92f082d 100644..100644
@@ -1,3 +1,13 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** The [ogit-write-config] executable.
4 Added:
5 Added: Writes a configuration file containing every variable at its default value,
6 Added: to the same location {!Ogit.Config.load} would read from. Intended for
7 Added: bootstrapping a new deployment.
8 Added:
9 Added: TODO: this overwrites the target file without warning and takes no
10 Added: arguments. Add a destination argument and refuse to clobber an existing
11 Added: file. *)
12 Added:
3 13 let () = Ogit.Config.(default |> to_table |> write_file)
bin/main.ml
index 1b5b0372..e69e2d1f 100644..100644
@@ -1,5 +1,12 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** The [ogit] executable.
4 Added:
5 Added: Parses arguments and delegates to {!Ogit.Main.run}. [--git-project-root]
6 Added: overrides the configured repository root and is enough to run without any
7 Added: config file, which is what makes a bare [ogit --git-project-root .] usable
8 Added: for a quick look at a local checkout. *)
9 Added:
3 10 let usage = "Usage: ogit [--git-project-root <path>]"
4 11
5 12 let () =
doc/conventions.mld
index 00000000..9810f8a4 000000..100644
@@ -0,0 +1,28 @@
1 Added: {0 Documentation conventions}
2 Added:
3 Added: {1 Where documentation lives}
4 Added:
5 Added: - A module with an [.mli] carries its documentation there, and only there. The
6 Added: [.ml] keeps comments that explain implementation choices. {!module-Ogit.Ui} is
7 Added: the reference example.
8 Added: - A module without an [.mli] documents itself in the [.ml].
9 Added: - Every module opens with a doc comment saying what it is responsible for, and
10 Added: what it deliberately does {i not} do when that boundary matters.
11 Added:
12 Added: {1 What is worth writing down}
13 Added:
14 Added: Prefer explaining a decision over restating a signature. [val port : int] needs
15 Added: no comment; the fact that a resolver failure keeps its category all the way to
16 Added: the HTTP boundary does.
17 Added:
18 Added: {1 Outstanding work}
19 Added:
20 Added: Items marked [TODO] in the sources are places where the intended behaviour is
21 Added: clear enough to use but has not been described. Find them with:
22 Added:
23 Added: {[
24 Added: grep -rn 'TODO' lib bin
25 Added: ]}
26 Added:
27 Added: The package metadata in [dune-project] also still carries dune's placeholder
28 Added: values for [description], [documentation] and [license].
doc/dune
index 00000000..0fea97dd 000000..100644
@@ -0,0 +1,8 @@
1 Added: ;; -*- mode: lisp; -*-
2 Added:
3 Added: ;; Attaches index.mld and conventions.mld to the ogit package, so that
4 Added: ;; `dune build @doc` uses index.mld as the package landing page instead of
5 Added: ;; dune's generated module list.
6 Added:
7 Added: (documentation
8 Added: (package ogit))
doc/index.mld
index 00000000..b8ac00a6 000000..100644
@@ -0,0 +1,55 @@
1 Added: {0 ogit}
2 Added:
3 Added: A lightweight, mobile-friendly alternative to cgit: a server-rendered web
4 Added: interface for browsing Git repositories. Pages work with JavaScript disabled and
5 Added: aim at WCAG 2.1 AA.
6 Added:
7 Added: {1 Reading order}
8 Added:
9 Added: Requests flow through the layers below in order. Start at {!module-Ogit.Routes}
10 Added: if you are tracing a URL, or at {!module-Ogit.Ui} if you are changing how a page
11 Added: looks.
12 Added:
13 Added: {2 Request handling}
14 Added:
15 Added: - {!module-Ogit.Routes} — bidirectional, type-safe paths shared by route
16 Added: registration and link generation.
17 Added: - {!module-Ogit.Handlers} — opens one repository context per request, maps
18 Added: errors to HTTP statuses, and pairs data access with rendering.
19 Added: - {!module-Ogit.Resolvers} — repository discovery and Git data access.
20 Added: - {!module-Ogit.Static_handler} — serves assets embedded at build time.
21 Added: - {!module-Ogit.Main} — loads configuration and starts the server.
22 Added:
23 Added: {2 Rendering}
24 Added:
25 Added: The view layer is split so that page code stays declarative.
26 Added:
27 Added: - {!module-Ogit.Ui} — the only module that names HTML elements. Generic and
28 Added: app-agnostic: a library of site building blocks.
29 Added: - {!module-Ogit.Components} — ogit's page parts, wired to {!module-Ogit.Routes}
30 Added: so URLs are never written by hand.
31 Added: - {!module-Ogit.Layout} — the page shell surrounding every page's content.
32 Added: - {!module-Ogit.Root} — the repository list and project directory pages.
33 Added: - {!module-Ogit.Repo} — the repository pages.
34 Added: - {!module-Ogit.Error} — error pages.
35 Added:
36 Added: {2 Supporting modules}
37 Added:
38 Added: - {!module-Ogit.Syntax} — guesses a blob's language for highlighting.
39 Added: - {!module-Ogit.Time_fmt} — formats Git dates for display.
40 Added: - {!module-Ogit.Diff} — computes line diffs between blobs.
41 Added: - {!module-Ogit.Charts} — generates standalone SVG charts.
42 Added: - {!module-Ogit.Config} — reads and validates the TOML configuration.
43 Added: - {!module-Ogit.List_ext} — small list helpers.
44 Added:
45 Added: {!module-Ogit.Static_assets} is generated at build time by [ocaml-crunch] from
46 Added: the contents of [lib/static/]; it carries no documentation of its own.
47 Added:
48 Added: {1 Building this documentation}
49 Added:
50 Added: {[
51 Added: dune build @doc
52 Added: ]}
53 Added:
54 Added: The entry point is [_build/default/_doc/_html/index.html].
55 Added: {!page-"conventions"} records the conventions this documentation follows.
lib/config.ml
index ed5793f4..bdee7e3b 100644..100644
@@ -1,4 +1,20 @@
1 1 (* -*- mode: tuareg; -*- *)
2 Added: (* -*- mode: tuareg; -*- *)
3 Added:
4 Added: (** Reading and validating the TOML configuration.
5 Added:
6 Added: The file is looked up at [$OGIT_CONFIG], then
7 Added: [$XDG_CONFIG_HOME/ogit/config.toml], then [/etc/ogit/config.toml]. When no
8 Added: file is chosen explicitly a missing one is fine and {!default} applies; a
9 Added: file that exists but is malformed or invalid is always an error, so a
10 Added: mistake never silently degrades into defaults.
11 Added:
12 Added: [config.toml] at the repository root documents every variable and doubles as
13 Added: the reference for its default. Adding a field here means updating that file
14 Added: in the same commit.
15 Added:
16 Added: {!to_table} and {!write_file} support the [ogit-write-config] executable,
17 Added: which emits a config file pre-filled with the defaults. *)
2 18
3 19 open Toml
4 20
lib/diff.ml
index 5613f587..953fc313 100644..100644
@@ -1,5 +1,19 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (* -*- mode: tuareg; -*- *)
4 Added:
5 Added: (** Line diffs between two blobs.
6 Added:
7 Added: Computes a longest-common-subsequence diff and groups the result into hunks
8 Added: with the surrounding context, which is the shape a reader expects from
9 Added: [git diff]. Large inputs fall back to a coarser result rather than spending
10 Added: unbounded time and memory on the LCS matrix.
11 Added:
12 Added: This module produces data only. Rendering it is {!module:Ui.Diff}'s job.
13 Added:
14 Added: TODO: document the exact size threshold and the shape of the fallback, once
15 Added: the limit is settled. *)
16 Added:
3 17 type line_kind = Context | Addition | Deletion
4 18
5 19 type line = {
lib/dune
index dc2ba8a3..24cbfd10 100644..100644
@@ -4,6 +4,9 @@
4 4
5 5 (library
6 6 (name ogit)
7 Added: ;; Public so that `dune build @doc` documents its modules: odoc skips
8 Added: ;; private libraries, which would leave the generated documentation empty.
9 Added: (public_name ogit)
7 10 (libraries dream dream-html git-unix toml)
8 11 (preprocess
9 12 (pps dream-html.ppx)))
lib/handlers.ml
index 4fb1b3a3..0d7c32bb 100644..100644
@@ -1,5 +1,25 @@
1 1 (* -*- mode: tuareg; -*- *)
2 Added: (* -*- mode: tuareg; -*- *)
2 3
4 Added: (** Request handlers: the seam between Git data and rendered pages.
5 Added:
6 Added: Each handler opens exactly one repository context, reads what its page
7 Added: needs, renders it, and closes the context — so the Git store, resolved
8 Added: metadata and default-branch policy are shared by every operation in a
9 Added: request instead of being reopened per query.
10 Added:
11 Added: Errors keep the category {!module:Resolvers} gave them until they reach
12 Added: {!error_response}, which is the single place a category becomes an HTTP
13 Added: status: malformed input is [400], a missing repository or object is [404],
14 Added: and storage failures are [500]. Handlers therefore never choose a status
15 Added: themselves.
16 Added:
17 Added: This is also the only layer allowed to touch both configuration and the
18 Added: filesystem; views receive plain values. *)
19 Added:
20 Added: (** Map a resolver failure to the status and wording shown to the reader.
21 Added: Internal failures are deliberately vague, since their detail is for the
22 Added: server log rather than the visitor. *)
3 23 let error_response error =
4 24 let status, title, message =
5 25 match error with
lib/main.ml
index cfbf1864..319374a7 100644..100644
@@ -1,5 +1,13 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** Server startup.
4 Added:
5 Added: Loads configuration, then hands the resulting routes to Dream. Startup fails
6 Added: loudly rather than falling back to defaults, so a typo in a config file is
7 Added: noticed immediately instead of silently changing which repositories are
8 Added: served. The one exception is an explicit [git_project_root] on the command
9 Added: line, which is enough on its own to run without any config file. *)
10 Added:
3 11 let run_with_config config =
4 12 Dream.run ~port:config.Config.port ~interface:config.Config.host
5 13 @@ Dream.logger
lib/routes.ml
index c8a530d7..36e654de 100644..100644
@@ -1,5 +1,16 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** URL paths, in both directions.
4 Added:
5 Added: One {!t} value describes a page, and the same value both generates a link
6 Added: ({!path_of}) and is recovered from an incoming request ({!dispatch}).
7 Added: Keeping the two directions in one module is what stops generated links and
8 Added: served routes from drifting apart.
9 Added:
10 Added: Only path-shaped routes live here. Query parameters — the commit list's
11 Added: filters and page number — are not modelled, because they refine a page
12 Added: rather than identify one. *)
13 Added:
3 14 type t =
4 15 | Root
5 16 | Project_dir of string
lib/static_handler.ml
index 7e71a2a0..55a93642 100644..100644
@@ -1,5 +1,15 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** Serving the static assets embedded at build time.
4 Added:
5 Added: Assets are compiled into the executable by [ocaml-crunch] (see the rule in
6 Added: [lib/dune]), so a deployment is a single binary with no asset directory to
7 Added: keep in sync. Responses are cached for a day; because the content ships with
8 Added: the binary, a new release is the only thing that can change them.
9 Added:
10 Added: TODO: the cache lifetime is hard-coded. Make it configurable, or switch to
11 Added: content-hashed asset URLs so the lifetime can be raised safely. *)
12 Added:
3 13 let content_type_of_ext = function
4 14 | ".css" -> "text/css; charset=utf-8"
5 15 | ".svg" -> "image/svg+xml"
lib/views/ui.mli
index 35e8d1ef..61fdc897 100644..100644
@@ -18,11 +18,12 @@
18 18 never domain-specific: a "tree" here is any hierarchical list, not a file
19 19 tree in particular. Optional [?class_] arguments add a modifier
20 20 {i alongside} the base class rather than replacing it.
21 Removed: - {b No scripting.} Interactive blocks ({!disclosure}, {!css_toggle}) rely
22 Removed: on native HTML and CSS, so pages stay usable with JavaScript disabled.
21 Added: - {b No scripting.} Interactive blocks ({!val-disclosure},
22 Added: {!val-css_toggle}) rely on native HTML and CSS, so pages stay usable with
23 Added: JavaScript disabled.
23 24 - {b Accessibility is not optional.} Where a block can only be used
24 25 correctly with an accessible name, that name is a required argument rather
25 Removed: than an optional one — see {!navigation} and {!dismissible}.
26 Added: than an optional one — see {!val-navigation} and {!val-dismissible}.
26 27
27 28 Nothing here performs I/O. The attribute plumbing that assembles these
28 29 blocks is deliberately not exported: callers compose blocks, they do not
@@ -58,7 +59,7 @@
58 59 information already available as text. *)
59 60
60 61 val inline_text : ?class_:string -> ?decorative:bool -> string -> node
61 Removed: (** {!inline} around a single string. *)
62 Added: (** {!val-inline} around a single string. *)
62 63
63 64 (** {1 Links} *)
64 65
@@ -205,7 +206,7 @@
205 206 (** {1 Breadcrumbs} *)
206 207
207 208 type crumb
208 Removed: (** One step in a trail. Build with {!crumb}. *)
209 Added: (** One step in a trail. Build with {!val-crumb}. *)
209 210
210 211 val crumb : ?href:string -> string -> crumb
211 212 (** A trail step. Without an href it renders as plain text, which is how the
@@ -230,7 +231,7 @@
230 231 (** {1 Navigation} *)
231 232
232 233 type nav_link
233 Removed: (** A destination in a navigation list. Build with {!nav_link}. *)
234 Added: (** A destination in a navigation list. Build with {!val-nav_link}. *)
234 235
235 236 val nav_link : ?current:bool -> href:string -> string -> nav_link
236 237
scripts/docs.sh
index 00000000..2b8d65d9 000000..100755
@@ -0,0 +1,37 @@
1 Added: #!/usr/bin/env bash
2 Added: # Build the odoc HTML documentation and open it in a browser.
3 Added: #
4 Added: # scripts/docs.sh build, then open
5 Added: # scripts/docs.sh --print build, then print the entry point path
6 Added: #
7 Added: # Equivalent to `dune build @doc`; this wrapper exists only to locate the entry
8 Added: # point, which is buried in the build directory.
9 Added:
10 Added: set -euo pipefail
11 Added:
12 Added: root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
13 Added: cd "$root"
14 Added:
15 Added: entry="_build/default/_doc/_html/index.html"
16 Added:
17 Added: opam exec -- dune build @doc
18 Added:
19 Added: if [[ ! -f $entry ]]; then
20 Added: printf 'error: expected documentation at %s\n' "$entry" >&2
21 Added: exit 1
22 Added: fi
23 Added:
24 Added: if [[ ${1-} == --print ]]; then
25 Added: printf '%s/%s\n' "$root" "$entry"
26 Added: exit 0
27 Added: fi
28 Added:
29 Added: url="file://$root/$entry"
30 Added:
31 Added: # xdg-open detaches, so suppress the browser's own chatter on stderr.
32 Added: if command -v xdg-open > /dev/null; then
33 Added: xdg-open "$url" > /dev/null 2>&1 &
34 Added: printf 'opened %s\n' "$url"
35 Added: else
36 Added: printf 'no xdg-open found; open this manually:\n%s\n' "$url"
37 Added: fi
test/test_config.ml
index bbf6010d..ce62c73f 100644..100644
@@ -1,5 +1,8 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** Configuration loading: round-tripping, backward compatibility, and rejection
4 Added: of malformed files and out-of-range values. *)
5 Added:
3 6 open Test_helpers
4 7
5 8 let test_round_trip () =
test/test_discovery.ml
index 384aefac..5900ad99 100644..100644
@@ -1,5 +1,8 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** Repository discovery: bare and non-bare layouts, descriptions, and directory
4 Added: listings. *)
5 Added:
3 6 open Test_helpers
4 7
5 8 let test_description_missing () =
test/test_dispatch.ml
index 8ad33ac7..7ad1f4d2 100644..100644
@@ -1,5 +1,8 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** URL dispatch: that {!Ogit.Routes.dispatch} recovers the intended repository
4 Added: and action, including from malformed input. *)
5 Added:
3 6 let action_to_string = function
4 7 | Ogit.Routes.Summary -> "Summary"
5 8 | Ogit.Routes.Commits_page -> "Commits_page"
test/test_list_ext.ml
index 322edfd2..1d1662a5 100644..100644
@@ -1,5 +1,7 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** The list helpers in {!Ogit.List_ext}. *)
4 Added:
3 5 let list_int = Alcotest.(list int)
4 6
5 7 let test_take () =
test/test_ogit.ml
index 3f7541e5..9bdc4bfa 100644..100644
@@ -1,5 +1,7 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** Test entry point. Registers every suite; add new suites here. *)
4 Added:
3 5 let () =
4 6 Alcotest.run "ogit"
5 7 [
test/test_router.ml
index a7390ccf..601e38fd 100644..100644
@@ -1,5 +1,8 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** Router error handling: that invalid names, hashes and missing objects
4 Added: produce the intended failure category. *)
5 Added:
3 6 open Test_helpers
4 7
5 8 let test_invalid_repo_name () =
test/test_tree_paths.ml
index 3fcf6ba1..ffd216e9 100644..100644
@@ -1,5 +1,7 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** Resolving paths within a Git tree, including unreachable objects. *)
4 Added:
3 5 open Test_helpers
4 6
5 7 let test_root_tree () =
test/test_validation.ml
index f967ce2f..8c08bf7b 100644..100644
@@ -1,5 +1,8 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** Input validation for repository names, hashes, branch names and tag names.
4 Added: *)
5 Added:
3 6 let test_valid_repo_names () =
4 7 Alcotest.(check bool)
5 8 "project.git" true
test/test_views.ml
index 79887416..164f4c2d 100644..100644
@@ -1,5 +1,8 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Added: (** View rendering: currently that the error page carries the requested HTTP
4 Added: status. *)
5 Added:
3 6 let test_error_page_status () =
4 7 let response =
5 8 Ogit.Views.error_page ~status:`Not_Found ~title:"Not found" "missing"