Repositories / gitweb2.git

gitweb2.git

Clone (read-only): git clone http://git.guha-anderson.com/git/gitweb2.git

Branch

Add --public-url and document the post-update hook situation

The advertised clone URL is derived from the request Host header by default,
which is wrong when the server is reached through a port-forward or reverse
proxy at a different host or scheme. --public-url overrides the base used in
the displayed clone command. README also explains that smart HTTP needs no
post-update hook; the hook is only relevant for dumb HTTP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
Arjun Guha <a.guha@northeastern.edu>
Date
2026-05-04 08:52:10 -0400
Commit
090d04e3b3e918e86a2b5b048fa20ad4e420b654
README.md
index a1374ef..104e806 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,12 @@ Options:
 dune exec gitweb2 -- ~/repos --host 127.0.0.1 --port 8080 --pygments "uv run --with pygments pygmentize"
 ```
 
+Use `--public-url` when the public address differs from the bind address (e.g. behind a port-forward or reverse proxy). It sets the base used in the displayed clone URL and overrides the request `Host` header:
+
+```sh
+dune exec gitweb2 -- ~/repos --host 0.0.0.0 --port 8002 --public-url https://git.example.com
+```
+
 Press `Ctrl+C` to stop the server.
 
 ## systemd (homebox)
@@ -66,3 +72,24 @@ binary must be on `PATH`. Push is not supported.
 ```sh
 git clone http://<host>:<port>/git/<repo>
 ```
+
+## Hooks: do you need `post-update`?
+
+No. Smart HTTP via `git http-backend` reads refs and packs from the live
+repository on every request, so the `info/refs` and `objects/info/packs`
+files are not consulted and there is nothing to refresh after a push.
+The traditional `post-update` hook is only needed for **dumb** HTTP, where
+the client fetches static files out of `.git/`.
+
+If you ever do want to expose a bare repo over dumb HTTP (or another tool
+expects up-to-date `info/` files), enable the sample hook git ships:
+
+```sh
+cd /path/to/repo.git
+mv hooks/post-update.sample hooks/post-update
+chmod +x hooks/post-update
+git update-server-info   # one-off, to seed the files
+```
+
+That hook just runs `git update-server-info` after each push. With gitweb2's
+smart HTTP route this is unnecessary.
service/README.md
index 90dae2d..28c8abe 100644
--- a/service/README.md
+++ b/service/README.md
@@ -26,4 +26,9 @@ For a repository with key `<repo>`, the URLs on this host are:
 - HTML viewer: `http://homebox:8002/repo/<repo>`
 - Read-only clone: `git clone http://homebox:8002/git/<repo>`
 
+If the service is reached publicly through a port-forward or reverse proxy at
+a different host or scheme, append `--public-url <url>` to the `ExecStart`
+line so the clone command shown on each repo page reflects the public URL
+(e.g. `--public-url https://git.example.com`).
+
 Without [lingering](https://www.freedesktop.org/software/systemd/man/latest/loginctl.html), the user service stops when you log out (`loginctl enable-linger "$USER"` to keep it).
src/gitweb2.ml
index c860c34..4e02b86 100644
--- a/src/gitweb2.ml
+++ b/src/gitweb2.ml
@@ -15,6 +15,7 @@ type config = {
   host : string;
   port : int;
   pygments_command : string;
+  public_url : string option;
 }
 
 type repo_info = {
@@ -562,8 +563,17 @@ let start (config : config) : unit =
         let query = Uri.verbatim_query uri |> Option.value ~default:"" in
         let req_headers = Cohttp.Request.headers request in
         let header value = Cohttp.Header.get req_headers value |> Option.value ~default:"" in
-        let host = header "host" in
-        let clone_base = if host = "" then None else Some ("http://" ^ host) in
+        let strip_trailing_slash s =
+          let n = String.length s in
+          if n > 0 && s.[n - 1] = '/' then String.sub s 0 (n - 1) else s
+        in
+        let clone_base =
+          match config.public_url with
+          | Some url -> Some (strip_trailing_slash url)
+          | None ->
+              let host = header "host" in
+              if host = "" then None else Some ("http://" ^ host)
+        in
         let response =
           if String.starts_with ~prefix:"/git/" path then
             let request_body =
@@ -595,24 +605,27 @@ let start (config : config) : unit =
   Cohttp_eio.Server.run socket server
     ~on_error:(fun exn -> Logs.err (fun m -> m "%s" (Printexc.to_string exn)))
 
-let usage : string = "usage: gitweb2 <repo-root> [--host 127.0.0.1] [--port 8080] [--pygments pygmentize]"
+let usage : string =
+  "usage: gitweb2 <repo-root> [--host 127.0.0.1] [--port 8080] [--pygments pygmentize] [--public-url \
+   https://git.example.com]"
 
 let parse_args (argv : string array) : (config, string) result =
   let args = Array.to_list argv |> List.tl in
   match args with
   | [] | ("-h" | "--help") :: _ -> Error usage
   | root :: rest ->
-      let rec loop host port pygments = function
-        | [] -> Ok { root; host; port; pygments_command = pygments }
-        | "--host" :: value :: tail -> loop value port pygments tail
+      let rec loop host port pygments public_url = function
+        | [] -> Ok { root; host; port; pygments_command = pygments; public_url }
+        | "--host" :: value :: tail -> loop value port pygments public_url tail
         | "--port" :: value :: tail -> (
             match int_of_string_opt value with
-            | Some port -> loop host port pygments tail
+            | Some port -> loop host port pygments public_url tail
             | None -> Error "missing or invalid --port value")
-        | "--pygments" :: value :: tail -> loop host port value tail
+        | "--pygments" :: value :: tail -> loop host port value public_url tail
+        | "--public-url" :: value :: tail -> loop host port pygments (Some value) tail
         | flag :: _ -> Error ("unknown argument " ^ flag)
       in
-      loop default_host default_port default_pygments_command rest
+      loop default_host default_port default_pygments_command None rest
 
 let run (argv : string array) : unit =
   match parse_args argv with
src/gitweb2.mli
index ace0a02..480a32a 100644
--- a/src/gitweb2.mli
+++ b/src/gitweb2.mli
@@ -9,6 +9,7 @@ type config = {
   host : string;
   port : int;
   pygments_command : string;
+  public_url : string option;
 }
 
 val default_host : string