Serve git smart-HTTP for bare /:owner/:name (no .git suffix)

636a67b · gmorell · 2026-06-25 14:55

2 files +25 -9
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