Serve git smart-HTTP for bare /:owner/:name (no .git suffix)
636a67b · gmorell · 2026-06-25 14:55
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud_web/plugs/git_router.ex
+19
−3
@@ -14,8 +14,8 @@ defmodule GitGudWeb.Plugs.GitRouter do
| 14 | 14 | def init(opts), do: opts |
| 15 | 15 | |
| 16 | 16 | @impl true |
| 17 | − def call(%Plug.Conn{path_info: [_owner, dot_git | _]} = conn, _opts) do | |
| 18 | − if String.ends_with?(dot_git, ".git") do | |
| 17 | + def call(%Plug.Conn{path_info: path_info} = conn, _opts) do | |
| 18 | + if git_request?(path_info) do | |
| 19 | 19 | conn |
| 20 | 20 | |> fetch_query_params() |
| 21 | 21 | |> GitSmartHttp.call(GitSmartHttp.init([])) |
@@ -25,5 +25,21 @@ defmodule GitGudWeb.Plugs.GitRouter do
| 25 | 25 | end |
| 26 | 26 | end |
| 27 | 27 | |
| 28 | − def call(conn, _opts), do: conn | |
| 28 | + # Divert git smart-HTTP (and LFS) requests to GitSmartHttp. Accept both | |
| 29 | + # the `/:owner/:name.git/...` form and the bare `/:owner/:name/...` | |
| 30 | + # form — `git clone <url>` and `actions/checkout` clone without a | |
| 31 | + # `.git` suffix, so we key off the git service sub-path too, not just | |
| 32 | + # the suffix. Bare `/:owner/:name/{info/refs,git-*,info/lfs}` doesn't | |
| 33 | + # collide with any web route (those live under /r, /orgs, /users, …). | |
| 34 | + defp git_request?([_owner, name | rest]) when is_binary(name) do | |
| 35 | + String.ends_with?(name, ".git") or git_service_path?(rest) | |
| 36 | + end | |
| 37 | + | |
| 38 | + defp git_request?(_), do: false | |
| 39 | + | |
| 40 | + defp git_service_path?(["info", "refs" | _]), do: true | |
| 41 | + defp git_service_path?(["git-upload-pack" | _]), do: true | |
| 42 | + defp git_service_path?(["git-receive-pack" | _]), do: true | |
| 43 | + defp git_service_path?(["info", "lfs" | _]), do: true | |
| 44 | + defp git_service_path?(_), do: false | |
| 29 | 45 | end |
modified
lib/git_gud_web/plugs/git_smart_http.ex
+6
−6
@@ -45,12 +45,12 @@ defmodule GitGudWeb.Plugs.GitSmartHttp do
| 45 | 45 | end |
| 46 | 46 | end |
| 47 | 47 | |
| 48 | − # /:owner/:name.git/<rest...> | |
| 49 | − defp split_repo_path([owner, dot_git | rest]) do | |
| 50 | − case String.split(dot_git, ".git", parts: 2) do | |
| 51 | − [name, ""] when name != "" -> {:ok, owner, name, rest} | |
| 52 | − _ -> :error | |
| 53 | − end | |
| 48 | + # /:owner/:name[.git]/<rest...> — the `.git` suffix is optional; git | |
| 49 | + # clients clone with or without it (GitRouter only routes real git | |
| 50 | + # service paths here, so a bare name is safe). Strip a trailing `.git` | |
| 51 | + # if present. | |
| 52 | + defp split_repo_path([owner, name | rest]) when owner != "" and name != "" do | |
| 53 | + {:ok, owner, String.replace_suffix(name, ".git", ""), rest} | |
| 54 | 54 | end |
| 55 | 55 | |
| 56 | 56 | defp split_repo_path(_), do: :error |
Parents: 8791df5