gitshark

Clone repository

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

← Commits

๐Ÿ’„ (repos): Move mirrors, danger zone & collaborators behind the Settings tab

b63e93dbb343d127f1e8aecf44abf138f08acb64 ยท Phillip Souza Furtner ยท 2026-07-09T13:44:18Z

Changes

12 files changed, +131 -94

MODIFY README.md +6 -5
diff --git a/README.md b/README.md
index 60d49e6..789393e 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
19 19 - push and private read authenticate with **personal access tokens** (HTTP Basic password)
20 20 - Clone/fetch/push over `ssh://git@<host>:2222/<owner>/<repo>.git`
21 21 - public-key authentication only; keys managed per user in the UI
22 -- 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 Collaborators 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
22 +- 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 23 - Per-repository issues: title, optional description (rendered as Markdown, XSS-safe), per-repo sequential number (`#1`, `#2`, โ€ฆ), and author; 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 24 - 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
25 25 - Issues auto-close from pushed commit messages, GitHub-style (`close(s|d)`/`fix(es|ed)`/`resolve(s|d)` + `#<number>`, e.g. `fixes #12`), over both HTTP and SSH pushes
@@ -31,8 +31,8 @@
31 31 - OIDC login (authorization code flow) via `GET /login`; on first login the user account is created without a username and the browser is redirected to `/onboarding`, where the user picks a URL-safe handle (`^[a-z0-9][a-z0-9-]{0,38}$`, unique across users and organisations). The chosen handle โ€” not the OIDC `preferred_username` claim (which is an SPN form in kanidm and not URL-safe) โ€” is used in all repo, SSH, ActivityPub, and webfinger URLs. The `name` claim becomes an editable display name; both can be changed later at `/settings/profile`. A request filter blocks all app pages until a handle is chosen. Logout is local-session only via `POST /logout` (the kanidm provider advertises no `end_session_endpoint`, so RP-Initiated Logout is disabled)
32 32 - Profile pictures: users can upload a PNG/JPEG/GIF/WebP avatar (โ‰ค 2 MB, content-type and magic bytes both validated) at `/settings/profile`, stored on the filesystem keyed by user UUID and served publicly at `GET /users/{username}/avatar`; shown wherever a local user is rendered (header nav, repo lists, repo sidebar, issue/MR/comment authors) via a reusable Qute avatar tag, removable, and falling back to an initials badge when absent. Git commit authors and remote federation actors are not local users and keep their existing pseudo-avatars
33 33 - **Collaborators** โ€” the repository owner can grant other local users read+write access
34 - (one flat role) on a per-repository settings page (`โ€ฆ/settings/collaborators`, linked in the
35 - repo sidebar for the owner). Collaborators can read and push โ€” UI, HTTP, and SSH alike โ€” even
34 + (one flat role) on a per-repository settings page (`โ€ฆ/settings/collaborators`, reached from a
35 + "Manage collaborators" link on the owner-only repository Settings page). Collaborators can read and push โ€” UI, HTTP, and SSH alike โ€” even
36 36 on private repositories, and manage issues and merge requests; deleting the repository,
37 37 managing mirrors, and managing collaborators stay owner-only. Guides:
38 38 [for users](docs/users/collaborators.md), [for admins](docs/admins/collaborators.md)
@@ -49,8 +49,9 @@
49 49 every push (`git push --mirror` semantics, all refs including deletions), over HTTPS with
50 50 stored credentials or over SSH with a server-generated Ed25519 deploy key. Syncs run
51 51 asynchronously (the incoming push never waits or fails because of a mirror), coalesce under
52 - rapid pushes, retry with exponential backoff, and dead-letter with the error visible in the
53 - UI; secrets are encrypted at rest (`GITSHARK_SECRET_KEY`). Guides:
52 + rapid pushes, retry with exponential backoff, and dead-letter with the error visible on the
53 + owner-only repository **Settings** page where mirrors are managed; secrets are encrypted at
54 + rest (`GITSHARK_SECRET_KEY`). Guides:
54 55 [for users](docs/users/mirrors.md), [for admins](docs/admins/mirrors.md),
55 56 [architecture](docs/maintainers/push-mirrors.md)
56 57 - **Federation (ForgeFed / ActivityPub)** โ€” *opt-in, off by default.* Public repositories are
MODIFY docs/maintainers/push-mirrors.md +1 -1
diff --git a/docs/maintainers/push-mirrors.md b/docs/maintainers/push-mirrors.md
index 4baebfa..4e43314 100644
--- a/docs/maintainers/push-mirrors.md
+++ b/docs/maintainers/push-mirrors.md
@@ -13,7 +13,7 @@
13 13 | `MirrorKeys` | `mirror/` | Ed25519 deploy-key generation (BouncyCastle), OpenSSH public-key rendering, PKCS#8 PEM round-trip (public key derives from the private key) |
14 14 | `MirrorUrlValidator` | `mirror/` | SSRF guard for targets (scheme, own-host loop check, non-public address rejection) |
15 15 | `web/MirrorResource` | `web/` | Owner-only form endpoints under `/repos/{owner}/{name}/mirrors`; non-owners get 404 |
16 -| Overview template | `RepositoryResource/overview.html` | Mirrors panel (owner-only): list + status, deploy-key display, add/push-now/delete forms |
16 +| Settings template | `RepositoryResource/settings.html` | Mirrors panel (owner-only, under the Settings tab): list + status, deploy-key display, add/push-now/delete forms; `MirrorResource` redirects back to `โ€ฆ/settings` |
17 17
18 18 ## Trigger flow
19 19
MODIFY docs/users/collaborators.md +2 -2
diff --git a/docs/users/collaborators.md b/docs/users/collaborators.md
index 2e0c82f..f44aef3 100644
--- a/docs/users/collaborators.md
+++ b/docs/users/collaborators.md
@@ -23,8 +23,8 @@
23 23
24 24 ## Adding a collaborator
25 25
26 -1. Open your repository. As the owner you'll find a **Collaborators** entry in the left
27 - sidebar (it links to `โ€ฆ/settings/collaborators`).
26 +1. Open your repository's **Settings** page (the โš™ Settings tab in the left sidebar,
27 + owner only) and click **Manage collaborators** (it opens `โ€ฆ/settings/collaborators`).
28 28 2. Enter the user's exact **username** (the handle used in their profile URLs โ€” there is
29 29 no search or autocomplete) and click **Add**.
30 30
MODIFY docs/users/mirrors.md +3 -2
diff --git a/docs/users/mirrors.md b/docs/users/mirrors.md
index 6d26f5b..a73635a 100644
--- a/docs/users/mirrors.md
+++ b/docs/users/mirrors.md
@@ -8,8 +8,9 @@
8 8
9 9 ## Adding a mirror
10 10
11 -Open your repository's overview page. As the owner you'll find a **Push mirrors** panel
12 -above the danger zone. Enter the remote URL and pick how git-shark should authenticate:
11 +Open your repository's **Settings** page (the โš™ Settings tab in the left sidebar, owner
12 +only). You'll find a **Push mirrors** panel above the danger zone. Enter the remote URL
13 +and pick how git-shark should authenticate:
13 14
14 15 ### HTTPS remote (`https://โ€ฆ`)
15 16
MODIFY src/main/java/de/workaround/web/MirrorResource.java +5 -5
diff --git a/src/main/java/de/workaround/web/MirrorResource.java b/src/main/java/de/workaround/web/MirrorResource.java
index c341f77..2f4578e 100644
--- a/src/main/java/de/workaround/web/MirrorResource.java
+++ b/src/main/java/de/workaround/web/MirrorResource.java
@@ -65,7 +65,7 @@
65 65 {
66 66 return badRequest(e.getMessage());
67 67 }
68 - return backToRepository(owner, name);
68 + return backToSettings(owner, name);
69 69 }
70 70
71 71 @POST
@@ -75,7 +75,7 @@
75 75 {
76 76 Owned owned = requireOwned(owner, name);
77 77 mirrorService.delete(owned.user(), owned.repository(), mirrorId);
78 - return backToRepository(owner, name);
78 + return backToSettings(owner, name);
79 79 }
80 80
81 81 @POST
@@ -85,7 +85,7 @@
85 85 {
86 86 Owned owned = requireOwned(owner, name);
87 87 mirrorService.pushNow(owned.user(), owned.repository(), mirrorId);
88 - return backToRepository(owner, name);
88 + return backToSettings(owner, name);
89 89 }
90 90
91 91 private record Owned(User user, Repository repository)
@@ -104,9 +104,9 @@
104 104 return new Owned(user, repository);
105 105 }
106 106
107 - private static Response backToRepository(String owner, String name)
107 + private static Response backToSettings(String owner, String name)
108 108 {
109 - return Response.seeOther(URI.create("/repos/" + owner + "/" + name)).build();
109 + return Response.seeOther(URI.create("/repos/" + owner + "/" + name + "/settings")).build();
110 110 }
111 111
112 112 private static Response badRequest(String message)
MODIFY src/main/java/de/workaround/web/RepositoryResource.java +13 -12
diff --git a/src/main/java/de/workaround/web/RepositoryResource.java b/src/main/java/de/workaround/web/RepositoryResource.java
index 7f0e334..a9cd77a 100644
--- a/src/main/java/de/workaround/web/RepositoryResource.java
+++ b/src/main/java/de/workaround/web/RepositoryResource.java
@@ -52,9 +52,9 @@
52 52 @CheckedTemplate
53 53 static class Templates
54 54 {
55 - static native TemplateInstance overview(Repository repo, RepoNav nav, boolean owner,
55 + static native TemplateInstance overview(Repository repo, RepoNav nav,
56 56 List<GitBrowseService.TreeEntry> entries, GitBrowseService.CommitInfo latestCommit, String latestCommitAge,
57 - String readmeName, String readmeHtml, List<de.workaround.model.PushMirror> mirrors);
57 + String readmeName, String readmeHtml);
58 58
59 59 static native TemplateInstance tree(Repository repo, RepoNav nav, String ref, String path,
60 60 List<GitBrowseService.TreeEntry> entries, List<Crumb> crumbs);
@@ -71,7 +71,8 @@
71 71
72 72 static native TemplateInstance tags(Repository repo, RepoNav nav, List<String> tags);
73 73
74 - static native TemplateInstance settings(Repository repo, RepoNav nav, String error);
74 + static native TemplateInstance settings(Repository repo, RepoNav nav, String error,
75 + List<de.workaround.model.PushMirror> mirrors);
75 76 }
76 77
77 78 @Inject
@@ -110,8 +111,6 @@
110 111 List<GitBrowseService.TreeEntry> entries = nav.empty()
111 112 ? List.of()
112 113 : browse.listTree(path, nav.defaultBranch(), "").orElse(List.of());
113 - User user = currentUser.get();
114 - boolean isOwner = accessPolicy.canAdmin(user, repo);
115 114 GitBrowseService.CommitInfo latestCommit = nav.empty() ? null
116 115 : browse.commits(path, nav.defaultBranch(), 0, 1)
117 116 .filter(commitPage -> !commitPage.commits().isEmpty())
@@ -124,9 +123,7 @@
124 123 .filter(blob -> !blob.binary())
125 124 .map(blob -> Markdown.render(new String(blob.content(), StandardCharsets.UTF_8)))
126 125 .orElse(null);
127 - List<de.workaround.model.PushMirror> mirrors = isOwner ? mirrorService.list(repo) : List.of();
128 - return Templates.overview(repo, nav, isOwner, entries, latestCommit, latestCommitAge, readmeName, readmeHtml,
129 - mirrors);
126 + return Templates.overview(repo, nav, entries, latestCommit, latestCommitAge, readmeName, readmeHtml);
130 127 }
131 128
132 129 // README file names the overview looks for, in order of preference (matched case-insensitively).
@@ -263,7 +260,7 @@
263 260 public TemplateInstance settings(@PathParam("owner") String owner, @PathParam("name") String name)
264 261 {
265 262 Repository repo = requireOwner(owner, name);
266 - return Templates.settings(repo, repoNav.build(repo, uriInfo), null);
263 + return Templates.settings(repo, repoNav.build(repo, uriInfo), null, mirrorService.list(repo));
267 264 }
268 265
269 266 @POST
@@ -276,7 +273,9 @@
276 273 if (image == null)
277 274 {
278 275 return Response.status(Response.Status.BAD_REQUEST)
279 - .entity(Templates.settings(repo, repoNav.build(repo, uriInfo), "No image was uploaded.")).build();
276 + .entity(Templates.settings(repo, repoNav.build(repo, uriInfo), "No image was uploaded.",
277 + mirrorService.list(repo)))
278 + .build();
280 279 }
281 280 try
282 281 {
@@ -286,7 +285,9 @@
286 285 catch (InvalidImageException e)
287 286 {
288 287 return Response.status(Response.Status.BAD_REQUEST)
289 - .entity(Templates.settings(repo, repoNav.build(repo, uriInfo), e.getMessage())).build();
288 + .entity(Templates.settings(repo, repoNav.build(repo, uriInfo), e.getMessage(),
289 + mirrorService.list(repo)))
290 + .build();
290 291 }
291 292 catch (IOException e)
292 293 {
@@ -310,7 +311,7 @@
310 311 {
311 312 return Response.status(Response.Status.BAD_REQUEST)
312 313 .entity(Templates.settings(repo, repoNav.build(repo, uriInfo),
313 - "Unknown visibility \"" + visibility + "\"."))
314 + "Unknown visibility \"" + visibility + "\".", mirrorService.list(repo)))
314 315 .build();
315 316 }
316 317 service.changeVisibility(currentUser.require(), repo, parsed);
MODIFY src/main/resources/templates/CollaboratorResource/collaborators.html +2 -1
diff --git a/src/main/resources/templates/CollaboratorResource/collaborators.html b/src/main/resources/templates/CollaboratorResource/collaborators.html
index ffdbbaa..9dee6e7 100644
--- a/src/main/resources/templates/CollaboratorResource/collaborators.html
+++ b/src/main/resources/templates/CollaboratorResource/collaborators.html
@@ -1,8 +1,9 @@
1 1 {#include layout}
2 2 {#title}Collaborators โ€“ {repo.name}{/title}
3 3 <div class="repo-layout">
4 - {#include RepositoryResource/sidebar nav=nav active='collaborators' /}
4 + {#include RepositoryResource/sidebar nav=nav active='settings' /}
5 5 <section class="repo-main">
6 + <p><a href="/repos/{repo.ownerHandle}/{repo.name}/settings">โ† Settings</a></p>
6 7 <h2>Collaborators</h2>
7 8 <p>Collaborators can read and push to this repository, including when it is private. Only you can manage them.</p>
8 9 {#if error}
MODIFY src/main/resources/templates/RepositoryResource/overview.html +0 -62
diff --git a/src/main/resources/templates/RepositoryResource/overview.html b/src/main/resources/templates/RepositoryResource/overview.html
index 5fcf195..deeee15 100644
--- a/src/main/resources/templates/RepositoryResource/overview.html
+++ b/src/main/resources/templates/RepositoryResource/overview.html
@@ -45,68 +45,6 @@
45 45 </div>
46 46 {/if}
47 47 {/if}
48 -
49 - {#if owner}
50 - <div class="panel mirrors">
51 - <div class="mirrors-head">Push mirrors</div>
52 - {#if mirrors}
53 - <ul class="mirror-list">
54 - {#for m in mirrors}
55 - <li class="mirror-row">
56 - <div class="mirror-url"><code>{m.remoteUrl}</code> <span class="tag">{m.authType.name()}</span></div>
57 - <div class="mirror-status meta">
58 - {#if m.lastError}
59 - <span class="mirror-error">Last attempt failed: {m.lastError}</span>
60 - {#else if m.lastSuccessAt}
61 - <span>Last synced {m.lastSuccessAt}</span>
62 - {#else}
63 - <span>Not synced yet</span>
64 - {/if}
65 - {#if m.lastAttemptAt}<span> ยท last attempt {m.lastAttemptAt}</span>{/if}
66 - </div>
67 - {#if m.publicKey}
68 - <div class="mirror-key">
69 - <p class="meta">Add this deploy key (with write access) at the remote:</p>
70 - <div class="mirror-deploy-key">
71 - <code>{m.publicKey}</code>
72 - <button type="button" class="btn-icon copy-btn" data-copy="{m.publicKey}" aria-label="Copy deploy key" title="Copy">โง‰</button>
73 - </div>
74 - </div>
75 - {/if}
76 - <div class="mirror-actions">
77 - <form class="inline" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors/{m.id}/push">
78 - <button class="btn">Push now</button>
79 - </form>
80 - <form class="inline" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors/{m.id}/delete">
81 - <button class="btn btn-danger">Delete</button>
82 - </form>
83 - </div>
84 - </li>
85 - {/for}
86 - </ul>
87 - {/if}
88 - <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors" class="mirror-add">
89 - <p><label>Remote URL <input name="url" placeholder="https://host/owner/repo.git or ssh://git@host/owner/repo.git" required></label></p>
90 - <p><label>Authentication
91 - <select name="authType">
92 - <option value="HTTPS">HTTPS โ€” username + password/token</option>
93 - <option value="SSH">SSH โ€” generated deploy key</option>
94 - </select>
95 - </label></p>
96 - <p><label>Username (HTTPS only) <input name="username" autocomplete="off"></label></p>
97 - <p><label>Password / token (HTTPS only) <input type="password" name="secret" autocomplete="new-password"></label></p>
98 - <button class="btn">Add mirror</button>
99 - </form>
100 - </div>
101 -
102 - <div class="danger-zone">
103 - <h2>Danger zone</h2>
104 - <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/delete">
105 - <p><label>Type the repository name to confirm deletion <input name="confirm"></label></p>
106 - <button class="btn btn-danger">Delete repository</button>
107 - </form>
108 - </div>
109 - {/if}
110 48 </section>
111 49
112 50 </div>
MODIFY src/main/resources/templates/RepositoryResource/settings.html +64 -0
diff --git a/src/main/resources/templates/RepositoryResource/settings.html b/src/main/resources/templates/RepositoryResource/settings.html
index a45ec58..faff655 100644
--- a/src/main/resources/templates/RepositoryResource/settings.html
+++ b/src/main/resources/templates/RepositoryResource/settings.html
@@ -42,6 +42,70 @@
42 42 <button class="btn btn-danger">Remove image</button>
43 43 </form>
44 44 {/if}
45 +
46 + <h2>Collaborators</h2>
47 + <p class="muted">Grant other users read and write access to this repository, including when it is private.</p>
48 + <p><a class="btn btn-secondary" href="/repos/{repo.ownerHandle}/{repo.name}/settings/collaborators">Manage collaborators</a></p>
49 +
50 + <div class="panel mirrors">
51 + <div class="mirrors-head">Push mirrors</div>
52 + {#if mirrors}
53 + <ul class="mirror-list">
54 + {#for m in mirrors}
55 + <li class="mirror-row">
56 + <div class="mirror-url"><code>{m.remoteUrl}</code> <span class="tag">{m.authType.name()}</span></div>
57 + <div class="mirror-status meta">
58 + {#if m.lastError}
59 + <span class="mirror-error">Last attempt failed: {m.lastError}</span>
60 + {#else if m.lastSuccessAt}
61 + <span>Last synced {m.lastSuccessAt}</span>
62 + {#else}
63 + <span>Not synced yet</span>
64 + {/if}
65 + {#if m.lastAttemptAt}<span> ยท last attempt {m.lastAttemptAt}</span>{/if}
66 + </div>
67 + {#if m.publicKey}
68 + <div class="mirror-key">
69 + <p class="meta">Add this deploy key (with write access) at the remote:</p>
70 + <div class="mirror-deploy-key">
71 + <code>{m.publicKey}</code>
72 + <button type="button" class="btn-icon copy-btn" data-copy="{m.publicKey}" aria-label="Copy deploy key" title="Copy">โง‰</button>
73 + </div>
74 + </div>
75 + {/if}
76 + <div class="mirror-actions">
77 + <form class="inline" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors/{m.id}/push">
78 + <button class="btn">Push now</button>
79 + </form>
80 + <form class="inline" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors/{m.id}/delete">
81 + <button class="btn btn-danger">Delete</button>
82 + </form>
83 + </div>
84 + </li>
85 + {/for}
86 + </ul>
87 + {/if}
88 + <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors" class="mirror-add">
89 + <p><label>Remote URL <input name="url" placeholder="https://host/owner/repo.git or ssh://git@host/owner/repo.git" required></label></p>
90 + <p><label>Authentication
91 + <select name="authType">
92 + <option value="HTTPS">HTTPS โ€” username + password/token</option>
93 + <option value="SSH">SSH โ€” generated deploy key</option>
94 + </select>
95 + </label></p>
96 + <p><label>Username (HTTPS only) <input name="username" autocomplete="off"></label></p>
97 + <p><label>Password / token (HTTPS only) <input type="password" name="secret" autocomplete="new-password"></label></p>
98 + <button class="btn">Add mirror</button>
99 + </form>
100 + </div>
101 +
102 + <div class="danger-zone">
103 + <h2>Danger zone</h2>
104 + <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/delete">
105 + <p><label>Type the repository name to confirm deletion <input name="confirm"></label></p>
106 + <button class="btn btn-danger">Delete repository</button>
107 + </form>
108 + </div>
45 109 </section>
46 110
47 111 </div>
MODIFY src/main/resources/templates/RepositoryResource/sidebar.html +0 -1
diff --git a/src/main/resources/templates/RepositoryResource/sidebar.html b/src/main/resources/templates/RepositoryResource/sidebar.html
index 4d0dec0..3164686 100644
--- a/src/main/resources/templates/RepositoryResource/sidebar.html
+++ b/src/main/resources/templates/RepositoryResource/sidebar.html
@@ -30,7 +30,6 @@
30 30 <a class="{#if active == 'merge-requests'}active{/if}" href="/repos/{nav.repo.ownerHandle}/{nav.repo.name}/merge-requests"><span class="g">โ‡„</span> Merge requests <span class="ct">{nav.openMrCount}</span></a>
31 31 {#if nav.isOwner}
32 32 <a class="{#if active == 'settings'}active{/if}" href="/repos/{nav.repo.ownerHandle}/{nav.repo.name}/settings"><span class="g">โš™</span> Settings</a>
33 - <a class="{#if active == 'collaborators'}active{/if}" href="/repos/{nav.repo.ownerHandle}/{nav.repo.name}/settings/collaborators"><span class="g">โš‡</span> Collaborators</a>
34 33 {/if}
35 34 </nav>
36 35 </aside>
MODIFY src/test/java/de/workaround/mirror/MirrorSettingsTest.java +27 -2
diff --git a/src/test/java/de/workaround/mirror/MirrorSettingsTest.java b/src/test/java/de/workaround/mirror/MirrorSettingsTest.java
index cb1941c..ee99796 100644
--- a/src/test/java/de/workaround/mirror/MirrorSettingsTest.java
+++ b/src/test/java/de/workaround/mirror/MirrorSettingsTest.java
@@ -68,7 +68,7 @@
68 68 assertEquals(PushMirror.AuthType.HTTPS, created.get(0).authType);
69 69
70 70 String page = given()
71 - .when().get("/repos/" + OWNER + "/" + repo.name)
71 + .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
72 72 .then().statusCode(200)
73 73 .extract().asString();
74 74 assertTrue(page.contains("https://mirror-target.example/owner/repo.git"), "mirror must be listed");
@@ -111,7 +111,7 @@
111 111 assertTrue(mirror.publicKey.startsWith("ssh-ed25519 "), "public key must be OpenSSH Ed25519");
112 112
113 113 given()
114 - .when().get("/repos/" + OWNER + "/" + repo.name)
114 + .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
115 115 .then().statusCode(200)
116 116 .body(containsString("ssh-ed25519 "))
117 117 .body(not(containsString("BEGIN PRIVATE KEY")));
@@ -214,10 +214,35 @@
214 214 Repository repo = repoOwnedBy(OWNER, "settings-hidden");
215 215 createMirror(repo);
216 216
217 + // mirrors live on the owner-only settings page; the repo settings route hides its existence
218 + // from non-owners (404), and the public overview shows nothing
217 219 given()
218 220 .when().get("/repos/" + OWNER + "/" + repo.name)
219 221 .then().statusCode(200)
220 222 .body(not(containsString("Push mirrors")));
223 +
224 + given()
225 + .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
226 + .then().statusCode(404);
227 + }
228 +
229 + @Test
230 + @TestSecurity(user = OWNER)
231 + void mirrorsAndDangerZoneLiveUnderSettingsNotOverview()
232 + {
233 + Repository repo = ownedRepo("settings-relocated");
234 +
235 + given()
236 + .when().get("/repos/" + OWNER + "/" + repo.name)
237 + .then().statusCode(200)
238 + .body(not(containsString("Push mirrors")))
239 + .body(not(containsString("Danger zone")));
240 +
241 + given()
242 + .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
243 + .then().statusCode(200)
244 + .body(containsString("Push mirrors"))
245 + .body(containsString("Danger zone"));
221 246 }
222 247
223 248 private Repository ownedRepo(String name)
MODIFY src/test/java/de/workaround/web/CollaboratorSettingsTest.java +8 -1
diff --git a/src/test/java/de/workaround/web/CollaboratorSettingsTest.java b/src/test/java/de/workaround/web/CollaboratorSettingsTest.java
index 5c80497..edd3886 100644
--- a/src/test/java/de/workaround/web/CollaboratorSettingsTest.java
+++ b/src/test/java/de/workaround/web/CollaboratorSettingsTest.java
@@ -219,13 +219,20 @@
219 219
220 220 @Test
221 221 @TestSecurity(user = OWNER)
222 - void ownerSeesSettingsLinkInSidebar()
222 + void collaboratorsAreHiddenBehindSettingsTabNotTheSidebar()
223 223 {
224 224 Repository repo = repoOwnedBy(OWNER, "collab-nav", Repository.Visibility.PUBLIC);
225 225
226 + // the sidebar only offers the Settings tab; collaborators is reached from the settings page
226 227 given()
227 228 .when().get("/repos/" + OWNER + "/" + repo.name)
228 229 .then().statusCode(200)
230 + .body(containsString("/settings\""))
231 + .body(not(containsString("settings/collaborators")));
232 +
233 + given()
234 + .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
235 + .then().statusCode(200)
229 236 .body(containsString("settings/collaborators"));
230 237 }
231 238

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog