woof
37d7949 · gmorell · 2026-05-15 04:31
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