woof

37d7949 · gmorell · 2026-05-15 04:31

3 files +61 -13

Files changed

modified config/config.exs
+8 −4
@@ -110,10 +110,14 @@ config :git_gud, GitGud.Ssh.Server,
110 110 enabled: false,
111 111 port: 2222,
112 112 host_key_dir: Path.expand("../priv/ssh", __DIR__),
113 # Hostname clients should connect to. Used by the UI to render
114 # `ssh://user@<host>:<port>/owner/repo.git`. Falls back to the
115 # endpoint URL's host.
116 external_host: nil
113 + # Hostname + port clients should connect to. Used by the UI to render
114 + # `ssh://user@<host>:<port>/owner/repo.git`. `external_host` falls
115 + # back to the endpoint URL's host; `external_port` falls back to
116 + # `:port`. Both are split from the internal listener so a deployment
117 + # can listen internally on 2222 while a front (Traefik, k8s LB,
118 + # nginx) maps the public port (e.g. 22222 or 22).
119 + external_host: nil,
120 + external_port: nil
117 121
118 122 # Repository storage. Override per-environment in dev.exs/runtime.exs.
119 123 config :git_gud, GitGud.Repositories.Storage, base_path: "priv/repos"
modified config/runtime.exs
+30 −0
@@ -77,6 +77,18 @@ if config_env() == :prod do
77 77 # multi-replica so every pod presents the same host fingerprint —
78 78 # otherwise SSH clients get a "remote host identification changed"
79 79 # warning when their load-balancer connection lands on a new pod.
80 + # `SSH_PORT` is what the embedded daemon listens on inside the pod.
81 + # `SSH_EXTERNAL_PORT` is what clients see — i.e., whatever port your
82 + # external LB / Traefik / ingress maps inbound to the daemon. The
83 + # UI renders the *external* port in clone URLs. Same idea for
84 + # `SSH_EXTERNAL_HOST` vs the Endpoint URL host.
85 + parse_port = fn key ->
86 + case System.get_env(key) do
87 + nil -> nil
88 + str -> String.to_integer(str)
89 + end
90 + end
91 +
80 92 ssh_overrides =
81 93 [enabled: System.get_env("SSH_SERVER_ENABLED") in ~w(true 1)]
82 94 |> Keyword.merge(
@@ -85,6 +97,24 @@ if config_env() == :prod do
85 97 dir -> [host_key_dir: dir]
86 98 end
87 99 )
100 + |> Keyword.merge(
101 + case parse_port.("SSH_PORT") do
102 + nil -> []
103 + p -> [port: p]
104 + end
105 + )
106 + |> Keyword.merge(
107 + case parse_port.("SSH_EXTERNAL_PORT") do
108 + nil -> []
109 + p -> [external_port: p]
110 + end
111 + )
112 + |> Keyword.merge(
113 + case System.get_env("SSH_EXTERNAL_HOST") do
114 + nil -> []
115 + h -> [external_host: h]
116 + end
117 + )
88 118
89 119 config :git_gud, GitGud.Ssh.Server, ssh_overrides
90 120
modified lib/git_gud_web/live/repo_live/show.ex
+23 −9
@@ -170,7 +170,7 @@ defmodule GitGudWeb.RepoLive.Show do
170 170 handle={@handle}
171 171 transport={@transport}
172 172 http_url={http_clone_url(@repo, @handle)}
173 ssh_url={ssh_clone_url(@repo, @handle)}
173 + ssh_url={ssh_clone_url(@repo, @handle, @current_scope)}
174 174 current_scope={@current_scope}
175 175 />
176 176 <% else %>
@@ -210,7 +210,7 @@ defmodule GitGudWeb.RepoLive.Show do
210 210 state={@clone_widget_state}
211 211 transport={@transport}
212 212 http_url={http_clone_url(@repo, @handle)}
213 ssh_url={ssh_clone_url(@repo, @handle)}
213 + ssh_url={ssh_clone_url(@repo, @handle, @current_scope)}
214 214 />
215 215 <span class="ml-auto text-xs opacity-60 font-mono">
216 216 {Git.short(@head_sha)}
@@ -572,7 +572,7 @@ defmodule GitGudWeb.RepoLive.Show do
572 572 GitGudWeb.Endpoint.url() <> "/" <> handle <> "/" <> repo.name <> ".git"
573 573 end
574 574
575 defp ssh_clone_url(repo, handle) do
575 + defp ssh_clone_url(repo, owner_handle, current_scope) do
576 576 ssh_cfg = Application.get_env(:git_gud, GitGud.Ssh.Server, [])
577 577
578 578 host =
@@ -580,11 +580,25 @@ defmodule GitGudWeb.RepoLive.Show do
580 580 URI.parse(GitGudWeb.Endpoint.url()).host ||
581 581 "localhost"
582 582
583 port = Keyword.get(ssh_cfg, :port, 2222)
584 # Username for the embedded daemon = the user's handle (email
585 # local-part of whoever registered the key). The page can't know
586 # which logged-in user is viewing — we render `<handle>` as a
587 # placeholder which is correct when you're cloning your own repo.
588 "ssh://#{handle}@#{host}:#{port}/#{handle}/#{repo.name}.git"
583 + # Prefer the explicit external port (what clients hit through the
584 + # LB / ingress); fall back to the internal listener port. The two
585 + # match for "expose the daemon directly" deploys and diverge when
586 + # something like Traefik maps a public 22222 → internal 2222.
587 + port =
588 + Keyword.get(ssh_cfg, :external_port) ||
589 + Keyword.get(ssh_cfg, :port, 2222)
590 +
591 + # The SSH `<user>` part is whoever is *connecting* — i.e., the
592 + # viewer's handle, validated by their key in /users/settings/ssh-
593 + # keys. It is NOT the repo owner's handle (those are independent
594 + # variables). Fall back to a `<your-handle>` placeholder when the
595 + # viewer is anonymous so it's obvious what to substitute.
596 + user =
597 + case current_scope do
598 + %{user: %{handle: h}} when is_binary(h) and h != "" -> h
599 + _ -> "<your-handle>"
600 + end
601 +
602 + "ssh://#{user}@#{host}:#{port}/#{owner_handle}/#{repo.name}.git"
589 603 end
590 604 end

Parents: 46f5ce8