gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

✨ (ssh): Advertise separate external SSH port with scp-shorthand URLs

a254338988844c57bca05b12d61f7aa10d32e785 · Michael Hainz · 2026-07-20T15:00:22Z

Changes

11 files changed, +241 -16

MODIFY README.md +4 -1
diff --git a/README.md b/README.md
index dd406b3..5ea983f 100644
--- a/README.md
+++ b/README.md
@@ -17,8 +17,11 @@
17 17 - Clone/fetch/push over `https://<host>/git/<owner>/<repo>.git`
18 18 - anonymous read on public repositories
19 19 - push and private read authenticate with **personal access tokens** (HTTP Basic password)
20 -- Clone/fetch/push over `ssh://git@<host>:2222/<owner>/<repo>.git`
20 +- Clone/fetch/push over SSH — scp shorthand `git@<host>:<owner>/<repo>.git` on the standard
21 + port, or `ssh://git@<host>:<port>/<owner>/<repo>.git` when the advertised port isn't 22
21 22 - public-key authentication only; keys managed per user in the UI
23 + - the server binds an unprivileged port (default 2222); the port shown in URLs is set
24 + separately via `GITSHARK_SSH_EXTERNAL_PORT` (default 22) to match your host mapping
22 25 - Web UI: an auth-aware header nav (a "Log in" button for visitors; for signed-in users a top-level Following link plus an Account dropdown holding Profile, SSH keys, Access tokens, and Logout — a JS-free `<details>` menu), landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser with self-hosted syntax highlighting (extension-based language detection, falls back to plain text for unknown extensions and binary files; Markdown files render to HTML by default with a Rendered/Code toggle), a rendered README (commonmark-java with GFM tables, XSS-safe) shown below the file list on the repository overview page, commit log (paginated), branches, tags (own dedicated page, separate from branches), one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Every repository sub-page shows a persistent left sidebar with repo identity, a Clone button opening the clone dialog, a pin toggle, and section navigation (Code, Commits, Branches, Tags, Issues, Merge requests, plus Settings for the owner) with per-section counts and active-section highlighting, and the clone panel has copy-to-clipboard buttons for the HTTP and SSH `git clone` commands. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) — every page works fully without JavaScript
23 26 - Per-repository issues: title, optional description (rendered as Markdown, XSS-safe), per-repo sequential number (`#1`, `#2`, …), an author, and an optional assignee (any local user, set by username; blank clears it); created and managed by the repo owner and collaborators, readable by anyone who can read the repo, via a dedicated "New issue" page; title and description can be edited afterwards via an "Edit issue" page. Issue pages are addressed by number (`…/issues/1`); old UUID URLs redirect permanently to the number form
24 27 - Issues move through a fixed lifecycle (Planned → In development → Done); the repo navigation shows the open (Planned + In development) issue count, and Done issues collapse into an "Archive" section on the issues page
MODIFY docs/README.md +3 -0
diff --git a/docs/README.md b/docs/README.md
index 86ecd69..8e5f84d 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -66,6 +66,9 @@
66 66
67 67 ### For maintainers
68 68
69 +- **[SSH transport & clone URLs](maintainers/ssh-transport.md)** — the embedded
70 + SSH server, the bind-port vs. advertised-external-port split, and how the
71 + `ssh://` clone/push URLs are constructed.
69 72 - **[Comments and per-commit diffs](maintainers/comments.md)** — how issue and
70 73 merge-request discussion comments and the per-commit diff view reuse the
71 74 existing comment and diff machinery, with their tables and endpoints.
MODIFY docs/admins/getting-started.md +26 -8
diff --git a/docs/admins/getting-started.md b/docs/admins/getting-started.md
index 31e983e..2effe31 100644
--- a/docs/admins/getting-started.md
+++ b/docs/admins/getting-started.md
@@ -24,7 +24,8 @@
24 24 - A **DNS name** pointing at the host (e.g. `gitshark.example.com`). OIDC redirect URIs
25 25 and — if you enable federation — permanent actor IDs are derived from it.
26 26 - An **OIDC provider** with an authorization-code client for git-shark. PKCE is required.
27 -- Ports **80/443** (reverse proxy) and **2222** (SSH git) reachable from your clients.
27 +- Ports **80/443** (reverse proxy) and the **SSH git** port (host **22** by default, mapped
28 + to container `2222`) reachable from your clients.
28 29
29 30 ---
30 31
@@ -201,9 +202,10 @@
201 202 GITSHARK_AVATAR_ROOT: /data/avatars
202 203 GITSHARK_REPO_IMAGE_ROOT: /data/repo-images
203 204 GITSHARK_SSH_HOST_KEY: /data/ssh/host-key
204 - GITSHARK_SSH_PORT: "2222"
205 + GITSHARK_SSH_PORT: "2222" # bind port INSIDE the container (>1024 → no root needed)
206 + GITSHARK_SSH_EXTERNAL_PORT: "22" # port shown in clone URLs; MUST match the published host port below
205 207 ports:
206 - - "2222:2222" # SSH git access, published directly
208 + - "22:2222" # publish SSH git on the standard port 22 (host 22 → container 2222)
207 209 volumes:
208 210 - repos:/data/repositories # bare git repositories
209 211 - avatars:/data/avatars # user profile pictures
@@ -238,7 +240,18 @@
238 240 (plus the database) are mandatory for a stateful deployment — see
239 241 [Persistent data](persistent-data.md) for what each holds and what breaks without it.
240 242 - **HTTP port 8080 is not published** — it's reached through the reverse proxy on the
241 - Compose network (Step 6). Only SSH (2222) is exposed directly.
243 + Compose network (Step 6). Only SSH is exposed directly (host **22** → container **2222**).
244 +- **Two SSH port knobs, and they are independent.** `GITSHARK_SSH_PORT` is the port the
245 + embedded server *binds inside the container* — keep it above 1024 (default `2222`) so the
246 + process never needs root. `GITSHARK_SSH_EXTERNAL_PORT` is *display only*: it is the port
247 + git-shark writes into the clone/push URLs shown in the UI (default `22`, and `22` is
248 + omitted from the printed URL for a clean `ssh://git@host/...`). It changes **no** runtime
249 + behaviour — the server still binds `GITSHARK_SSH_PORT`. **You are responsible for making
250 + the two agree with your port publishing:** the external port must equal the host port you
251 + publish. Above we bind `2222`, publish it on host `22`, and advertise `22` — consistent.
252 + If the host already runs its own sshd on `22`, publish the container on `2222` instead
253 + (`"2222:2222"`) **and** set `GITSHARK_SSH_EXTERNAL_PORT: "2222"` so the advertised URL
254 + matches what clients can actually reach.
242 255 - **Single app replica.** git-shark keeps git state on a `ReadWriteOnce`-style filesystem
243 256 volume; do not scale `app` beyond one instance.
244 257 - Flyway migrates the schema automatically at startup (`migrate-at-start=true`), so the
@@ -311,11 +324,15 @@
311 324 **SSH** (public-key only; add your key under *SSH keys* in the UI):
312 325
313 326 ```bash
314 -git clone ssh://git@gitshark.example.com:2222/<owner>/<repo>.git
327 +git clone git@gitshark.example.com:<owner>/<repo>.git
315 328 ```
316 329
317 -> Want bare `git@gitshark.example.com` without the `:2222`? Publish the container's 2222
318 -> on host port 22 (`"22:2222"`) — but only if the host's own sshd isn't already using 22.
330 +> On the default port `GITSHARK_SSH_EXTERNAL_PORT=22` the UI shows the short scp-like form
331 +> above (`git@host:owner/repo.git`, exactly like GitHub) — matching the `"22:2222"` publish.
332 +> The scp shorthand **cannot carry a port**, so if you publish SSH on a non-standard host
333 +> port (e.g. `"2222:2222"`), set `GITSHARK_SSH_EXTERNAL_PORT` to that port and the UI falls
334 +> back to the explicit form `ssh://git@gitshark.example.com:2222/...`. The external port is
335 +> display-only; it must equal the reachable host port or the copy-paste clone command fails.
319 336
320 337 ---
321 338
@@ -338,7 +355,8 @@
338 355 | `GITSHARK_AVATAR_ROOT` | — | `data/avatars` | On-disk profile-picture (avatar) storage root |
339 356 | `GITSHARK_REPO_IMAGE_ROOT` | — | `data/repo-images` | On-disk per-repository image storage root |
340 357 | `GITSHARK_SSH_HOST_KEY` | — | `data/ssh/host-key` | Persistent SSH host key path |
341 -| `GITSHARK_SSH_PORT` | — | `2222` | Embedded SSH server port |
358 +| `GITSHARK_SSH_PORT` | — | `2222` | Port the embedded SSH server **binds inside the container**; keep >1024 so it needs no root |
359 +| `GITSHARK_SSH_EXTERNAL_PORT` | — | `22` | Port advertised in clone/push URLs (display only, no runtime effect). Must match the published host port; `22` is omitted from the printed URL |
342 360 | `GITSHARK_SECRET_KEY` | — | — | Encrypts push-mirror secrets at rest; required to create mirrors (see [Push mirrors](mirrors.md)) |
343 361 | `GITSHARK_MIRROR_MAX_ATTEMPTS` | — | `8` | Mirror-sync retry cap before dead-letter |
344 362 | `GITSHARK_MIRROR_ALLOW_INSECURE` | — | `false` | Dev only: allow http/loopback mirror targets |
ADD docs/maintainers/ssh-transport.md +73 -0
diff --git a/docs/maintainers/ssh-transport.md b/docs/maintainers/ssh-transport.md
new file mode 100644
index 0000000..601802a
--- /dev/null
+++ b/docs/maintainers/ssh-transport.md
@@ -0,0 +1,73 @@
1 +# SSH transport & clone-URL construction
2 +
3 +How git-shark serves Git over SSH, and how the SSH clone/push URLs shown in the
4 +UI are built. For the admin-facing configuration, see
5 +[Getting started → Configuration reference](../admins/getting-started.md#configuration-reference).
6 +
7 +## Component map
8 +
9 +| Component | Responsibility |
10 +|---|---|
11 +| `de.workaround.ssh.GitSshServer` | Starts the embedded Apache SSHD daemon at boot, binds `gitshark.ssh.port`, persists the host key. Public-key auth only; no passwords, no shell. |
12 +| `de.workaround.ssh.GitSshAuthenticator` | Resolves a presented public key to a user via `SshKeyService`. |
13 +| `de.workaround.ssh.GitSshCommandFactory` | Maps the incoming `git-upload-pack` / `git-receive-pack` command to a repository. |
14 +| `de.workaround.web.CloneUrls` | Pure helper that renders the `ssh://` URL string shown in the UI. |
15 +| `de.workaround.web.RepoNavService` | Assembles the `RepoNav` record (clone URLs, counts, …) rendered by the repo sidebar and empty-repo quick start. |
16 +
17 +## Bind port vs. external (advertised) port — the key decision
18 +
19 +There are **two** SSH port config values, and they are deliberately decoupled:
20 +
21 +- `gitshark.ssh.port` (env `GITSHARK_SSH_PORT`, default **2222**) — the port the
22 + embedded server actually **binds** (`GitSshServer.onStart` → `sshd.setPort(port)`).
23 + Kept above 1024 by default so the container process never needs `CAP_NET_BIND_SERVICE`
24 + or root to listen.
25 +- `gitshark.ssh.external-port` (env `GITSHARK_SSH_EXTERNAL_PORT`, default **22**) — the
26 + port written into the **advertised** clone/push URLs (`RepoNavService` injects it as
27 + `sshExternalPort` and passes it to `CloneUrls.ssh(...)`). It has **no runtime effect**
28 + on the server; it exists only so the URL can reflect a host/ingress port mapping.
29 +
30 +**Why the split exists.** The common deployment publishes the container's `2222` on host
31 +port `22` (`"22:2222"` in Compose, or a `Service`/ingress mapping in Kubernetes). Clients
32 +then connect on the clean standard port, but the process inside the container still binds
33 +an unprivileged port. Before this split the UI printed the *bind* port (`2222`), producing
34 +URLs like `ssh://git@host:2222/...` even when SSH was reachable on `22` — wrong and ugly.
35 +Advertising a separate external port fixes both.
36 +
37 +**Invariant the admin must uphold:** `external-port` must equal the host port SSH is
38 +actually reachable on. git-shark cannot verify this — it only renders the string. A
39 +mismatch yields a copy-paste clone command that connects to a closed port. This is called
40 +out in the admin docs; keep both docs consistent if the defaults change.
41 +
42 +## URL rendering rules (`CloneUrls`)
43 +
44 +- `CloneUrls.DEFAULT_SSH_PORT = 22`. When `external-port == 22` it emits the short
45 + **scp-like** form `git@host:owner/repo.git` (as GitHub does) — no `ssh://`, no port.
46 + Any other port forces the explicit `ssh://git@host:2222/owner/repo.git` form, because the
47 + scp shorthand cannot express a port (git would parse the number as part of the path).
48 +- The **host** comes from the incoming web request (`uriInfo.getBaseUri().getHost()`),
49 + not a separate config value — SSH is assumed to share the web hostname. There is no
50 + independent SSH-host override today (see gaps below).
51 +- The **HTTP(S)** clone URL is still derived purely from the request base URI
52 + (`uriInfo.getBaseUri().resolve(...)`), which already normalises away default ports
53 + (80/443). Only the SSH URL needed the explicit port handling.
54 +
55 +Both URLs land on the `RepoNav` record (`httpUrl`, `sshUrl`) and are rendered by
56 +`templates/RepositoryResource/sidebar.html` (clone dialog) and
57 +`templates/RepositoryResource/overview.html` (empty-repo quick start, HTTPS/SSH toggle).
58 +
59 +## What works today
60 +
61 +- Embedded public-key SSH Git transport on a configurable, unprivileged bind port.
62 +- Separate advertised SSH port so URLs match a host/ingress port mapping; on port 22 the
63 + URL uses the short scp-like `git@host:owner/repo.git` form.
64 +- Empty-repo quick start offers both HTTPS and SSH push commands via a client-side toggle.
65 +
66 +## What still needs to be implemented
67 +
68 +- **Separate SSH hostname.** The SSH host is hard-tied to the web request host. A
69 + deployment terminating SSH on a different name (e.g. `ssh.gitshark.example`) cannot
70 + advertise it — would need a `gitshark.ssh.external-host` config, threaded through
71 + `CloneUrls` the same way `external-port` is.
72 +- **Runtime validation** that `external-port` is actually reachable — currently purely
73 + advisory via docs.
ADD src/main/java/de/workaround/web/CloneUrls.java +32 -0
diff --git a/src/main/java/de/workaround/web/CloneUrls.java b/src/main/java/de/workaround/web/CloneUrls.java
new file mode 100644
index 0000000..22aad5e
--- /dev/null
+++ b/src/main/java/de/workaround/web/CloneUrls.java
@@ -0,0 +1,32 @@
1 +package de.workaround.web;
2 +
3 +/**
4 + * Builds the clone/remote URLs shown in the UI. Kept separate from the request-derived HTTP URL so the SSH URL can use
5 + * a configured <em>external</em> port that may differ from the port the embedded server binds inside the container.
6 + * On the default port (22) it emits the short scp-like form ({@code git@host:owner/repo.git}, as GitHub does); any
7 + * other port forces the explicit {@code ssh://host:port/...} form, because scp shorthand cannot express a port.
8 + */
9 +public final class CloneUrls
10 +{
11 + /** The port git assumes for {@code ssh://} URLs when none is given. */
12 + public static final int DEFAULT_SSH_PORT = 22;
13 +
14 + private CloneUrls()
15 + {
16 + }
17 +
18 + /**
19 + * @param host the externally reachable SSH host (the web request host)
20 + * @param externalPort the port users connect to (may differ from the container bind port); {@code 22} is omitted
21 + * @param owner the repository owner handle
22 + * @param repoName the repository name (without {@code .git})
23 + */
24 + public static String ssh(String host, int externalPort, String owner, String repoName)
25 + {
26 + if (externalPort == DEFAULT_SSH_PORT)
27 + {
28 + return "git@" + host + ":" + owner + "/" + repoName + ".git";
29 + }
30 + return "ssh://git@" + host + ":" + externalPort + "/" + owner + "/" + repoName + ".git";
31 + }
32 +}
MODIFY src/main/java/de/workaround/web/RepoNavService.java +5 -4
diff --git a/src/main/java/de/workaround/web/RepoNavService.java b/src/main/java/de/workaround/web/RepoNavService.java
index 509c057..e005e56 100644
--- a/src/main/java/de/workaround/web/RepoNavService.java
+++ b/src/main/java/de/workaround/web/RepoNavService.java
@@ -45,8 +45,10 @@
45 45 @Inject
46 46 MergeRequestService mergeRequestService;
47 47
48 - @ConfigProperty(name = "gitshark.ssh.port")
49 - int sshPort;
48 + // The port shown in clone URLs. This is the port users actually connect to (e.g. 22), which may differ from
49 + // gitshark.ssh.port — the port the embedded server binds inside the container (e.g. 2222, so it needs no root).
50 + @ConfigProperty(name = "gitshark.ssh.external-port")
51 + int sshExternalPort;
50 52
51 53 public RepoNav build(Repository repo, UriInfo uriInfo)
52 54 {
@@ -67,8 +69,7 @@
67 69 long openMrCount = mergeRequestService.countOpen(repo);
68 70 String httpUrl = uriInfo.getBaseUri().resolve("/git/" + repo.ownerHandle() + "/" + repo.name + ".git")
69 71 .toString();
70 - String sshUrl = "ssh://git@" + uriInfo.getBaseUri().getHost() + ":" + sshPort + "/" + repo.ownerHandle()
71 - + "/" + repo.name + ".git";
72 + String sshUrl = CloneUrls.ssh(uriInfo.getBaseUri().getHost(), sshExternalPort, repo.ownerHandle(), repo.name);
72 73 return new RepoNav(repo, loggedIn, isOwner, pinned, empty, defaultBranch, commitCount, branchCount, tagCount,
73 74 openIssueCount, openMrCount, httpUrl, sshUrl, uriInfo.getRequestUri().getRawPath(), parentVisible);
74 75 }
MODIFY src/main/resources/META-INF/resources/shark.css +39 -0
diff --git a/src/main/resources/META-INF/resources/shark.css b/src/main/resources/META-INF/resources/shark.css
index 9db220f..2af5c0d 100644
--- a/src/main/resources/META-INF/resources/shark.css
+++ b/src/main/resources/META-INF/resources/shark.css
@@ -2000,6 +2000,45 @@
2000 2000 display: flex;
2001 2001 }
2002 2002
2003 +/* empty-repo quick start */
2004 +
2005 +.quickstart > input[type="radio"] {
2006 + position: absolute;
2007 + opacity: 0;
2008 + pointer-events: none;
2009 +}
2010 +
2011 +.qs-tabs {
2012 + display: flex;
2013 + gap: 6px;
2014 + margin-bottom: var(--s2);
2015 +}
2016 +
2017 +.qs-tabs label {
2018 + font: 600 12px/1 var(--mono);
2019 + color: var(--muted);
2020 + border: 1px solid var(--border);
2021 + border-radius: 999px;
2022 + padding: 7px 16px;
2023 + cursor: pointer;
2024 +}
2025 +
2026 +#qs-http:checked ~ .qs-tabs label[for="qs-http"],
2027 +#qs-ssh:checked ~ .qs-tabs label[for="qs-ssh"] {
2028 + color: #fff;
2029 + background: var(--accent);
2030 + border-color: var(--accent);
2031 +}
2032 +
2033 +.qs-cmd {
2034 + display: none;
2035 +}
2036 +
2037 +#qs-http:checked ~ .qs-http-cmd,
2038 +#qs-ssh:checked ~ .qs-ssh-cmd {
2039 + display: block;
2040 +}
2041 +
2003 2042 /* main column */
2004 2043
2005 2044 .branchbar {
MODIFY src/main/resources/application.properties +7 -0
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index c424c35..9988859 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -103,7 +103,14 @@
103 103 %test.gitshark.storage.repo-images=target/test-repo-images
104 104
105 105 # Embedded SSH server
106 +# port the embedded SSH server binds inside the container; keep it >1024 so the process needs no root
106 107 gitshark.ssh.port=${GITSHARK_SSH_PORT:2222}
108 +# port advertised in clone URLs — the externally reachable SSH port (e.g. 22 via a host/ingress port mapping).
109 +# Display only; changing it does NOT change the bind port above. 22 is omitted from the printed URL.
110 +gitshark.ssh.external-port=${GITSHARK_SSH_EXTERNAL_PORT:22}
111 +# In dev the server binds 2222 directly (no host port-mapping), so advertise 2222 too —
112 +# otherwise the UI would print the port-22 scp shorthand while dev SSH listens on 2222.
113 +%dev.gitshark.ssh.external-port=2222
107 114 gitshark.ssh.host-key-path=${GITSHARK_SSH_HOST_KEY:data/ssh/host-key}
108 115 %test.gitshark.ssh.port=0
109 116 %test.gitshark.ssh.host-key-path=target/test-ssh/host-key
MODIFY src/main/resources/templates/RepositoryResource/overview.html +11 -1
diff --git a/src/main/resources/templates/RepositoryResource/overview.html b/src/main/resources/templates/RepositoryResource/overview.html
index deeee15..8f11800 100644
--- a/src/main/resources/templates/RepositoryResource/overview.html
+++ b/src/main/resources/templates/RepositoryResource/overview.html
@@ -9,8 +9,18 @@
9 9 {#if nav.empty}
10 10 <h2>Quick start</h2>
11 11 <p>This repository is empty. Push an existing repository:</p>
12 - <pre>git remote add origin {nav.httpUrl}
12 + <div class="quickstart">
13 + <input type="radio" name="qs-proto" id="qs-http" checked>
14 + <input type="radio" name="qs-proto" id="qs-ssh">
15 + <div class="qs-tabs">
16 + <label for="qs-http">HTTPS</label>
17 + <label for="qs-ssh">SSH</label>
18 + </div>
19 + <pre class="qs-cmd qs-http-cmd">git remote add origin {nav.httpUrl}
13 20 git push -u origin main</pre>
21 + <pre class="qs-cmd qs-ssh-cmd">git remote add origin {nav.sshUrl}
22 +git push -u origin main</pre>
23 + </div>
14 24 {#else}
15 25 <div class="branchbar">
16 26 <a class="branchsel" href="/repos/{repo.ownerHandle}/{repo.name}/branches"><span class="g">⑂</span> {nav.defaultBranch} <span class="car">▾</span></a>
ADD src/test/java/de/workaround/web/CloneUrlsTest.java +24 -0
diff --git a/src/test/java/de/workaround/web/CloneUrlsTest.java b/src/test/java/de/workaround/web/CloneUrlsTest.java
new file mode 100644
index 0000000..51e2aed
--- /dev/null
+++ b/src/test/java/de/workaround/web/CloneUrlsTest.java
@@ -0,0 +1,24 @@
1 +package de.workaround.web;
2 +
3 +import org.junit.jupiter.api.Test;
4 +
5 +import static org.junit.jupiter.api.Assertions.assertEquals;
6 +
7 +class CloneUrlsTest
8 +{
9 + @Test
10 + void sshUrlUsesScpShorthandForDefaultPort()
11 + {
12 + // scp-like shorthand (like GitHub) — shortest form, but git only accepts it on port 22.
13 + assertEquals("git@shark.example:workaround/gitshark.git",
14 + CloneUrls.ssh("shark.example", 22, "workaround", "gitshark"));
15 + }
16 +
17 + @Test
18 + void sshUrlKeepsNonDefaultPort()
19 + {
20 + // scp shorthand cannot express a port, so a non-default port forces the ssh:// form.
21 + assertEquals("ssh://git@shark.example:2222/workaround/gitshark.git",
22 + CloneUrls.ssh("shark.example", 2222, "workaround", "gitshark"));
23 + }
24 +}
MODIFY src/test/java/de/workaround/web/WebUiTest.java +17 -2
diff --git a/src/test/java/de/workaround/web/WebUiTest.java b/src/test/java/de/workaround/web/WebUiTest.java
index 7445c00..d4438e9 100644
--- a/src/test/java/de/workaround/web/WebUiTest.java
+++ b/src/test/java/de/workaround/web/WebUiTest.java
@@ -427,7 +427,22 @@
427 427 given().when().get("/repos/" + owner.username + "/cloneurls")
428 428 .then().statusCode(200)
429 429 .body(containsString("/git/" + owner.username + "/cloneurls.git"))
430 - .body(containsString("ssh://git@"));
430 + .body(containsString("git@localhost:" + owner.username + "/cloneurls.git"));
431 + }
432 +
433 + @Test
434 + void emptyRepositoryQuickStartOffersBothProtocols() throws Exception
435 + {
436 + User owner = persistUser("ui-quick-" + unique());
437 + service.create(owner, "emptyqs", Repository.Visibility.PUBLIC, null);
438 +
439 + given().when().get("/repos/" + owner.username + "/emptyqs")
440 + .then().statusCode(200)
441 + .body(containsString("class=\"quickstart\""))
442 + .body(containsString("id=\"qs-http\""))
443 + .body(containsString("id=\"qs-ssh\""))
444 + .body(containsString("git remote add origin git@localhost:" + owner.username + "/emptyqs.git"))
445 + .body(containsString("git push -u origin main"));
431 446 }
432 447
433 448 @Test
@@ -521,7 +536,7 @@
521 536 // ...with both protocols selectable...
522 537 .body(containsString("git clone "))
523 538 .body(containsString("/git/" + owner.username + "/clonedialog.git"))
524 - .body(containsString("ssh://git@"))
539 + .body(containsString("git@localhost:" + owner.username + "/clonedialog.git"))
525 540 // ...and no longer pinned in the header topbar or sidebar
526 541 .body(not(containsString("class=\"clonecmd\"")))
527 542 .body(not(containsString("class=\"clone-urls\"")));

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog