gitshark

Clone repository

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

← Commits

✨ (ui): Add repo tab navigation, restyle remaining pages, self-host fonts

17e386f62af4cec8da9ba6c96e57120fc834e5f3 · Michael Hainz · 2026-07-07T12:30:09Z

Changes

27 files changed, +451 -38

MODIFY .github/workflows/native.yml +2 -2
diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml
index 47087df..3221165 100644
--- a/.github/workflows/native.yml
+++ b/.github/workflows/native.yml
@@ -15,5 +15,5 @@
15 15 distribution: temurin
16 16 java-version: '21'
17 17 cache: maven
18 - - name: Native build
19 - run: ./mvnw -B package -Dnative -DskipTests -Dquarkus.native.container-build=true
18 + - name: Native build & integration tests
19 + run: ./mvnw -B verify -Dnative -Dquarkus.native.container-build=true
MODIFY README.md +2 -1
diff --git a/README.md b/README.md
index 0af4df0..132af78 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
14 14 - push and private read authenticate with **personal access tokens** (HTTP Basic password)
15 15 - Clone/fetch/push over `ssh://git@<host>:2222/<owner>/<repo>.git`
16 16 - public-key authentication only; keys managed per user in the UI
17 -- Web UI: 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), commit log (paginated), branches & tags, one-time handle selection (`/onboarding`), profile settings (`/settings/profile`)
17 +- Web UI: 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), commit log (paginated), branches & tags, one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Repository pages share Files / Commits / Branches tab navigation that preserves the selected ref. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) — every page works fully without JavaScript
18 18 - 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). 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)
19 19 - Single access policy on all paths: owner read/write, public world-readable, private owner-only
20 20 - **Federation (ForgeFed / ActivityPub)** — *opt-in, off by default.* Public repositories are
@@ -56,6 +56,7 @@
56 56 - The SSH host key is generated on first start and persisted, so the host identity is
57 57 stable across restarts.
58 58 - SSH serves only `git-upload-pack`/`git-receive-pack`; shells and other commands are rejected.
59 +- UI fonts (Space Grotesk, JetBrains Mono) are self-hosted from the application origin — no CDN or other external requests, so the UI works fully offline/air-gapped.
59 60
60 61 ## Configuration
61 62
MODIFY openspec/changes/add-dashboard-pinned-notifications/tasks.md +1 -1
diff --git a/openspec/changes/add-dashboard-pinned-notifications/tasks.md b/openspec/changes/add-dashboard-pinned-notifications/tasks.md
index a2d88f2..742b81b 100644
--- a/openspec/changes/add-dashboard-pinned-notifications/tasks.md
+++ b/openspec/changes/add-dashboard-pinned-notifications/tasks.md
@@ -37,5 +37,5 @@
37 37 ## 6. Verify
38 38
39 39 - [x] 6.1 Run `./mvnw test` — 69/70 green. The one failure (`SettingsPagesTest.anonymousIsRedirectedToLogin`) is a pre-existing Keycloak Dev Service startup flake ("OIDC Server is not available"), unrelated to this change; all new tests pass
40 -- [ ] 6.2 Manual check in `quarkus:dev`: pin/unpin reflects in the pinned section; notifications shows the empty state; full list still lists all visible repos; anonymous `/` unchanged
40 +- [x] 6.2 Manual check in `quarkus:dev`: pin/unpin reflects in the pinned section; notifications shows the empty state; full list still lists all visible repos; anonymous `/` unchanged
41 41 - [x] 6.3 Update `README.md` for the user-facing dashboard/pinning behavior and the new `repository_pins` table
MODIFY openspec/changes/add-user-onboarding/tasks.md +1 -1
diff --git a/openspec/changes/add-user-onboarding/tasks.md b/openspec/changes/add-user-onboarding/tasks.md
index 2dea421..8811275 100644
--- a/openspec/changes/add-user-onboarding/tasks.md
+++ b/openspec/changes/add-user-onboarding/tasks.md
@@ -32,4 +32,4 @@
32 32 ## 6. Verify
33 33
34 34 - [x] 6.1 Run full test suite green; format (`mvn formatter:format` + `mvn impsort:sort`)
35 -- [ ] 6.2 Manual dev check: fresh OIDC subject → onboarding → handle in repo/AP/webfinger URLs; rename via settings; seeded alice unaffected
35 +- [x] 6.2 Manual dev check: fresh OIDC subject → onboarding → handle in repo/AP/webfinger URLs; rename via settings; seeded alice unaffected
MODIFY openspec/changes/ui-design-system/design.md +5 -0
diff --git a/openspec/changes/ui-design-system/design.md b/openspec/changes/ui-design-system/design.md
index c07ce98..e6f3a81 100644
--- a/openspec/changes/ui-design-system/design.md
+++ b/openspec/changes/ui-design-system/design.md
@@ -62,6 +62,11 @@
62 62 on every modern OS with zero payload and zero native-image config; revisit only if the rendered result disappoints.
63 63 This satisfies "self-hosted, no CDN" trivially — there are no font requests at all.
64 64
65 +**Addendum (2026-07-07):** the rendered result did disappoint — during implementation the UI adopted Space Grotesk +
66 +JetBrains Mono, initially via Google Fonts CDN, which violated the "no external font requests" requirement. Resolved by
67 +self-hosting both families as variable WOFF2 (latin + latin-ext, ~97 KB total) under `META-INF/resources/fonts/` with
68 +`@font-face` in `shark.css` and OFL license texts alongside. System stacks remain as fallbacks in the font tokens.
69 +
65 70 ### D4: Tabs are links, active state from a template parameter
66 71
67 72 The repo templates already render per-section pages; each repository page passes an `activeTab` string into the
MODIFY openspec/changes/ui-design-system/tasks.md +9 -9
diff --git a/openspec/changes/ui-design-system/tasks.md b/openspec/changes/ui-design-system/tasks.md
index 10ceb79..f820f2f 100644
--- a/openspec/changes/ui-design-system/tasks.md
+++ b/openspec/changes/ui-design-system/tasks.md
@@ -15,23 +15,23 @@
15 15 ## 3. Restyle templates
16 16
17 17 - [x] 3.1 Restyle `HomeResource/home.html` and `newRepo.html`: repo table with visibility badges, primary button for "new repository", styled form (home additionally got a hero section with the shark logo)
18 -- [ ] 3.2 Restyle `RepositoryResource/overview.html`, `tree.html`, `blob.html`: clone URLs and file content in monospace code blocks, empty-repo setup instructions as styled code block
19 -- [ ] 3.3 Restyle `RepositoryResource/commits.html` and `branches.html`: monospace hashes/ref names, default-branch badge, secondary-button pagination
20 -- [ ] 3.4 Restyle `SettingsResource/keys.html`, `tokens.html`, `tokenCreated.html`: styled forms, danger buttons for delete actions, monospace fingerprints/tokens
18 +- [x] 3.2 Restyle `RepositoryResource/overview.html`, `tree.html`, `blob.html`: clone URLs and file content in monospace code blocks, empty-repo setup instructions as styled code block
19 +- [x] 3.3 Restyle `RepositoryResource/commits.html` and `branches.html`: monospace hashes/ref names, default-branch badge, secondary-button pagination
20 +- [x] 3.4 Restyle `SettingsResource/keys.html`, `tokens.html`, `tokenCreated.html`: styled forms, danger buttons for delete actions, monospace fingerprints/tokens
21 21
22 22 ## 4. Repository tab navigation
23 23
24 -- [ ] 4.1 Write failing tests: repo pages render Files/Commits/Branches tabs with correct active tab, and tab links preserve the selected ref
25 -- [ ] 4.2 Pass an `activeTab` parameter from `RepositoryResource` page methods to the templates
26 -- [ ] 4.3 Add the tab markup (shared Qute fragment or per-template) using the `.tabs` component; make 4.1 green
24 +- [x] 4.1 Write failing tests: repo pages render Files/Commits/Branches tabs with correct active tab, and tab links preserve the selected ref
25 +- [x] 4.2 Pass an `activeTab` parameter from `RepositoryResource` page methods to the templates
26 +- [x] 4.3 Add the tab markup (shared Qute fragment or per-template) using the `.tabs` component; make 4.1 green
27 27
28 28 ## 5. Hotkeys (progressive enhancement)
29 29
30 30 - [x] 5.1 Create `src/main/resources/META-INF/resources/shark-hotkeys.js`: key map (`?` help, `Escape` close, `g h` home), form-field guard, ~1s sequence timeout per D5; include via `<script defer>` in `layout.html`
31 -- [ ] 5.2 Write test asserting pages render and forms work without JavaScript (server-side rendering unaffected by the script tag)
31 +- [x] 5.2 Write test asserting pages render and forms work without JavaScript (server-side rendering unaffected by the script tag)
32 32
33 33 ## 6. Verification
34 34
35 -- [ ] 6.1 Run full test suite via bishbash; all green
36 -- [ ] 6.2 Manual pass over all 11 pages: light theme consistent, tabs/buttons/badges correct, hotkeys work, `?` overlay opens/closes
35 +- [x] 6.1 Run full test suite via bishbash; all green
36 +- [x] 6.2 Manual pass over all 11 pages: light theme consistent, tabs/buttons/badges correct, hotkeys work, `?` overlay opens/closes
37 37 - [ ] 6.3 Verify native image still builds and serves `/shark.css` + `/shark-hotkeys.js` (META-INF/resources inclusion per D6)
MODIFY src/main/java/de/workaround/web/RepositoryResource.java +9 -8
diff --git a/src/main/java/de/workaround/web/RepositoryResource.java b/src/main/java/de/workaround/web/RepositoryResource.java
index bc5ef36..6af8566 100644
--- a/src/main/java/de/workaround/web/RepositoryResource.java
+++ b/src/main/java/de/workaround/web/RepositoryResource.java
@@ -50,16 +50,16 @@
50 50 int branchCount, int tagCount);
51 51
52 52 static native TemplateInstance tree(Repository repo, String ref, String path,
53 - List<GitBrowseService.TreeEntry> entries, List<Crumb> crumbs);
53 + List<GitBrowseService.TreeEntry> entries, List<Crumb> crumbs, String activeTab);
54 54
55 55 static native TemplateInstance blob(Repository repo, String ref, String path, boolean binary, String content,
56 - String language, List<Crumb> crumbs);
56 + String language, List<Crumb> crumbs, String activeTab);
57 57
58 58 static native TemplateInstance commits(Repository repo, String ref, List<GitBrowseService.CommitInfo> commits,
59 - int page, int prevPage, int nextPage, int size, boolean hasNext);
59 + int page, int prevPage, int nextPage, int size, boolean hasNext, String activeTab);
60 60
61 61 static native TemplateInstance branches(Repository repo, List<GitBrowseService.BranchInfo> branches,
62 - List<String> tags);
62 + List<String> tags, String activeTab, String tabRef);
63 63 }
64 64
65 65 @Inject
@@ -167,7 +167,7 @@
167 167 Repository repo = requireReadable(owner, name);
168 168 Path repoPath = service.repositoryPath(repo);
169 169 return browse.listTree(repoPath, ref, path)
170 - .map(entries -> Templates.tree(repo, ref, path, entries, breadcrumbs(repo, ref, path)))
170 + .map(entries -> Templates.tree(repo, ref, path, entries, breadcrumbs(repo, ref, path), "files"))
171 171 .orElseGet(() -> blobView(repo, repoPath, ref, path));
172 172 }
173 173
@@ -196,7 +196,7 @@
196 196 .commits(service.repositoryPath(repo), ref, boundedPage, boundedSize)
197 197 .orElseThrow(NotFoundException::new);
198 198 return Templates.commits(repo, ref, commitPage.commits(), boundedPage, boundedPage - 1, boundedPage + 1,
199 - boundedSize, commitPage.hasNext());
199 + boundedSize, commitPage.hasNext(), "commits");
200 200 }
201 201
202 202 @GET
@@ -205,7 +205,8 @@
205 205 {
206 206 Repository repo = requireReadable(owner, name);
207 207 Path path = service.repositoryPath(repo);
208 - return Templates.branches(repo, browse.branches(path), browse.tags(path));
208 + String tabRef = browse.isEmpty(path) ? null : browse.defaultBranch(path);
209 + return Templates.branches(repo, browse.branches(path), browse.tags(path), "branches", tabRef);
209 210 }
210 211
211 212 @POST
@@ -261,7 +262,7 @@
261 262 GitBrowseService.BlobView blob = browse.blob(repoPath, ref, path).orElseThrow(NotFoundException::new);
262 263 String content = blob.binary() ? null : new String(blob.content(), StandardCharsets.UTF_8);
263 264 String language = blob.binary() ? null : highlightLanguage(path);
264 - return Templates.blob(repo, ref, path, blob.binary(), content, language, breadcrumbs(repo, ref, path));
265 + return Templates.blob(repo, ref, path, blob.binary(), content, language, breadcrumbs(repo, ref, path), "files");
265 266 }
266 267
267 268 // Extension → highlight.js language id. Every value here MUST have a grammar in the bundled highlight assets
ADD src/main/resources/META-INF/resources/fonts/OFL-JetBrainsMono.txt +93 -0
diff --git a/src/main/resources/META-INF/resources/fonts/OFL-JetBrainsMono.txt b/src/main/resources/META-INF/resources/fonts/OFL-JetBrainsMono.txt
new file mode 100644
index 0000000..5ceee00
--- /dev/null
+++ b/src/main/resources/META-INF/resources/fonts/OFL-JetBrainsMono.txt
@@ -0,0 +1,93 @@
1 +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
2 +
3 +This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 +This license is copied below, and is also available with a FAQ at:
5 +https://openfontlicense.org
6 +
7 +
8 +-----------------------------------------------------------
9 +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 +-----------------------------------------------------------
11 +
12 +PREAMBLE
13 +The goals of the Open Font License (OFL) are to stimulate worldwide
14 +development of collaborative font projects, to support the font creation
15 +efforts of academic and linguistic communities, and to provide a free and
16 +open framework in which fonts may be shared and improved in partnership
17 +with others.
18 +
19 +The OFL allows the licensed fonts to be used, studied, modified and
20 +redistributed freely as long as they are not sold by themselves. The
21 +fonts, including any derivative works, can be bundled, embedded,
22 +redistributed and/or sold with any software provided that any reserved
23 +names are not used by derivative works. The fonts and derivatives,
24 +however, cannot be released under any other type of license. The
25 +requirement for fonts to remain under this license does not apply
26 +to any document created using the fonts or their derivatives.
27 +
28 +DEFINITIONS
29 +"Font Software" refers to the set of files released by the Copyright
30 +Holder(s) under this license and clearly marked as such. This may
31 +include source files, build scripts and documentation.
32 +
33 +"Reserved Font Name" refers to any names specified as such after the
34 +copyright statement(s).
35 +
36 +"Original Version" refers to the collection of Font Software components as
37 +distributed by the Copyright Holder(s).
38 +
39 +"Modified Version" refers to any derivative made by adding to, deleting,
40 +or substituting -- in part or in whole -- any of the components of the
41 +Original Version, by changing formats or by porting the Font Software to a
42 +new environment.
43 +
44 +"Author" refers to any designer, engineer, programmer, technical
45 +writer or other person who contributed to the Font Software.
46 +
47 +PERMISSION & CONDITIONS
48 +Permission is hereby granted, free of charge, to any person obtaining
49 +a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 +redistribute, and sell modified and unmodified copies of the Font
51 +Software, subject to the following conditions:
52 +
53 +1) Neither the Font Software nor any of its individual components,
54 +in Original or Modified Versions, may be sold by itself.
55 +
56 +2) Original or Modified Versions of the Font Software may be bundled,
57 +redistributed and/or sold with any software, provided that each copy
58 +contains the above copyright notice and this license. These can be
59 +included either as stand-alone text files, human-readable headers or
60 +in the appropriate machine-readable metadata fields within text or
61 +binary files as long as those fields can be easily viewed by the user.
62 +
63 +3) No Modified Version of the Font Software may use the Reserved Font
64 +Name(s) unless explicit written permission is granted by the corresponding
65 +Copyright Holder. This restriction only applies to the primary font name as
66 +presented to the users.
67 +
68 +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 +Software shall not be used to promote, endorse or advertise any
70 +Modified Version, except to acknowledge the contribution(s) of the
71 +Copyright Holder(s) and the Author(s) or with their explicit written
72 +permission.
73 +
74 +5) The Font Software, modified or unmodified, in part or in whole,
75 +must be distributed entirely under this license, and must not be
76 +distributed under any other license. The requirement for fonts to
77 +remain under this license does not apply to any document created
78 +using the Font Software.
79 +
80 +TERMINATION
81 +This license becomes null and void if any of the above conditions are
82 +not met.
83 +
84 +DISCLAIMER
85 +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 +OTHER DEALINGS IN THE FONT SOFTWARE.
ADD src/main/resources/META-INF/resources/fonts/OFL-SpaceGrotesk.txt +93 -0
diff --git a/src/main/resources/META-INF/resources/fonts/OFL-SpaceGrotesk.txt b/src/main/resources/META-INF/resources/fonts/OFL-SpaceGrotesk.txt
new file mode 100644
index 0000000..cb512b9
--- /dev/null
+++ b/src/main/resources/META-INF/resources/fonts/OFL-SpaceGrotesk.txt
@@ -0,0 +1,93 @@
1 +Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
2 +
3 +This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 +This license is copied below, and is also available with a FAQ at:
5 +http://scripts.sil.org/OFL
6 +
7 +
8 +-----------------------------------------------------------
9 +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 +-----------------------------------------------------------
11 +
12 +PREAMBLE
13 +The goals of the Open Font License (OFL) are to stimulate worldwide
14 +development of collaborative font projects, to support the font creation
15 +efforts of academic and linguistic communities, and to provide a free and
16 +open framework in which fonts may be shared and improved in partnership
17 +with others.
18 +
19 +The OFL allows the licensed fonts to be used, studied, modified and
20 +redistributed freely as long as they are not sold by themselves. The
21 +fonts, including any derivative works, can be bundled, embedded,
22 +redistributed and/or sold with any software provided that any reserved
23 +names are not used by derivative works. The fonts and derivatives,
24 +however, cannot be released under any other type of license. The
25 +requirement for fonts to remain under this license does not apply
26 +to any document created using the fonts or their derivatives.
27 +
28 +DEFINITIONS
29 +"Font Software" refers to the set of files released by the Copyright
30 +Holder(s) under this license and clearly marked as such. This may
31 +include source files, build scripts and documentation.
32 +
33 +"Reserved Font Name" refers to any names specified as such after the
34 +copyright statement(s).
35 +
36 +"Original Version" refers to the collection of Font Software components as
37 +distributed by the Copyright Holder(s).
38 +
39 +"Modified Version" refers to any derivative made by adding to, deleting,
40 +or substituting -- in part or in whole -- any of the components of the
41 +Original Version, by changing formats or by porting the Font Software to a
42 +new environment.
43 +
44 +"Author" refers to any designer, engineer, programmer, technical
45 +writer or other person who contributed to the Font Software.
46 +
47 +PERMISSION & CONDITIONS
48 +Permission is hereby granted, free of charge, to any person obtaining
49 +a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 +redistribute, and sell modified and unmodified copies of the Font
51 +Software, subject to the following conditions:
52 +
53 +1) Neither the Font Software nor any of its individual components,
54 +in Original or Modified Versions, may be sold by itself.
55 +
56 +2) Original or Modified Versions of the Font Software may be bundled,
57 +redistributed and/or sold with any software, provided that each copy
58 +contains the above copyright notice and this license. These can be
59 +included either as stand-alone text files, human-readable headers or
60 +in the appropriate machine-readable metadata fields within text or
61 +binary files as long as those fields can be easily viewed by the user.
62 +
63 +3) No Modified Version of the Font Software may use the Reserved Font
64 +Name(s) unless explicit written permission is granted by the corresponding
65 +Copyright Holder. This restriction only applies to the primary font name as
66 +presented to the users.
67 +
68 +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 +Software shall not be used to promote, endorse or advertise any
70 +Modified Version, except to acknowledge the contribution(s) of the
71 +Copyright Holder(s) and the Author(s) or with their explicit written
72 +permission.
73 +
74 +5) The Font Software, modified or unmodified, in part or in whole,
75 +must be distributed entirely under this license, and must not be
76 +distributed under any other license. The requirement for fonts to
77 +remain under this license does not apply to any document created
78 +using the Font Software.
79 +
80 +TERMINATION
81 +This license becomes null and void if any of the above conditions are
82 +not met.
83 +
84 +DISCLAIMER
85 +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 +OTHER DEALINGS IN THE FONT SOFTWARE.
ADD src/main/resources/META-INF/resources/fonts/README.md +10 -0
diff --git a/src/main/resources/META-INF/resources/fonts/README.md b/src/main/resources/META-INF/resources/fonts/README.md
new file mode 100644
index 0000000..54e75b7
--- /dev/null
+++ b/src/main/resources/META-INF/resources/fonts/README.md
@@ -0,0 +1,10 @@
1 +# Vendored fonts
2 +
3 +Variable WOFF2 fonts self-hosted so the UI makes no external requests (see openspec `ui-design-system`, design D3).
4 +
5 +| Family | Files | Version | Source | License |
6 +|---|---|---|---|---|
7 +| Space Grotesk | `space-grotesk-latin.woff2`, `space-grotesk-latin-ext.woff2` | v22 (Google Fonts API) | https://github.com/floriankarsten/space-grotesk | [OFL-SpaceGrotesk.txt](OFL-SpaceGrotesk.txt) |
8 +| JetBrains Mono | `jetbrains-mono-latin.woff2`, `jetbrains-mono-latin-ext.woff2` | v24 (Google Fonts API) | https://github.com/JetBrains/JetBrainsMono | [OFL-JetBrainsMono.txt](OFL-JetBrainsMono.txt) |
9 +
10 +Downloaded 2026-07-07 from fonts.gstatic.com (latin + latin-ext subsets, variable weight axes). `@font-face` declarations live in `../shark.css`.
ADD src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
diff --git a/src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin-ext.woff2 b/src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin-ext.woff2
new file mode 100644
index 0000000..b1f5804
--- /dev/null
+++ b/src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin-ext.woff2
0 0 Binary files differ
ADD src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin.woff2 +0 -0
diff --git a/src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin.woff2 b/src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin.woff2
new file mode 100644
index 0000000..c3f0666
--- /dev/null
+++ b/src/main/resources/META-INF/resources/fonts/jetbrains-mono-latin.woff2
0 0 Binary files differ
ADD src/main/resources/META-INF/resources/fonts/space-grotesk-latin-ext.woff2 +0 -0
diff --git a/src/main/resources/META-INF/resources/fonts/space-grotesk-latin-ext.woff2 b/src/main/resources/META-INF/resources/fonts/space-grotesk-latin-ext.woff2
new file mode 100644
index 0000000..e3aa61c
--- /dev/null
+++ b/src/main/resources/META-INF/resources/fonts/space-grotesk-latin-ext.woff2
0 0 Binary files differ
ADD src/main/resources/META-INF/resources/fonts/space-grotesk-latin.woff2 +0 -0
diff --git a/src/main/resources/META-INF/resources/fonts/space-grotesk-latin.woff2 b/src/main/resources/META-INF/resources/fonts/space-grotesk-latin.woff2
new file mode 100644
index 0000000..7b0e76a
--- /dev/null
+++ b/src/main/resources/META-INF/resources/fonts/space-grotesk-latin.woff2
0 0 Binary files differ
MODIFY src/main/resources/META-INF/resources/shark.css +43 -1
diff --git a/src/main/resources/META-INF/resources/shark.css b/src/main/resources/META-INF/resources/shark.css
index ce6a3dc..d2cc949 100644
--- a/src/main/resources/META-INF/resources/shark.css
+++ b/src/main/resources/META-INF/resources/shark.css
@@ -1,5 +1,42 @@
1 1 /* git-shark design system — warm canvas, deep-teal accent, Space Grotesk + JetBrains Mono */
2 2
3 +/* self-hosted variable fonts (no CDN requests; native image ships them from META-INF/resources) */
4 +@font-face {
5 + font-family: "Space Grotesk";
6 + font-style: normal;
7 + font-weight: 300 700;
8 + font-display: swap;
9 + src: url(/fonts/space-grotesk-latin.woff2) format('woff2');
10 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
11 +}
12 +
13 +@font-face {
14 + font-family: "Space Grotesk";
15 + font-style: normal;
16 + font-weight: 300 700;
17 + font-display: swap;
18 + src: url(/fonts/space-grotesk-latin-ext.woff2) format('woff2');
19 + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
20 +}
21 +
22 +@font-face {
23 + font-family: "JetBrains Mono";
24 + font-style: normal;
25 + font-weight: 100 800;
26 + font-display: swap;
27 + src: url(/fonts/jetbrains-mono-latin.woff2) format('woff2');
28 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
29 +}
30 +
31 +@font-face {
32 + font-family: "JetBrains Mono";
33 + font-style: normal;
34 + font-weight: 100 800;
35 + font-display: swap;
36 + src: url(/fonts/jetbrains-mono-latin-ext.woff2) format('woff2');
37 + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
38 +}
39 +
3 40 :root {
4 41 /* surfaces */
5 42 --canvas: oklch(0.985 0.004 80);
@@ -713,12 +750,17 @@
713 750 border-radius: var(--radius);
714 751 }
715 752
716 -td.actions {
753 +td.actions, th.actions {
717 754 width: 1%;
718 755 white-space: nowrap;
719 756 text-align: right;
720 757 }
721 758
759 +.pagination {
760 + display: flex;
761 + gap: var(--s2);
762 +}
763 +
722 764 td.actions .btn {
723 765 font-size: 0.8rem;
724 766 }
MODIFY src/main/resources/templates/RepositoryResource/blob.html +2 -1
diff --git a/src/main/resources/templates/RepositoryResource/blob.html b/src/main/resources/templates/RepositoryResource/blob.html
index 5b264f7..d84b9f6 100644
--- a/src/main/resources/templates/RepositoryResource/blob.html
+++ b/src/main/resources/templates/RepositoryResource/blob.html
@@ -1,6 +1,7 @@
1 1 {#include layout}
2 2 {#title}{path} at {ref} – {repo.name}{/title}
3 3 <h1><a href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></h1>
4 +{#include RepositoryResource/tabs repo=repo tabRef=ref activeTab=activeTab /}
4 5 <nav class="breadcrumb" aria-label="Path">
5 6 {#for crumb in crumbs}
6 7 {#if crumb.href}<a href="{crumb.href}">{crumb.label}</a>{#else}<span class="current">{crumb.label}</span>{/if}
@@ -8,7 +9,7 @@
8 9 {/for}
9 10 </nav>
10 11 {#if binary}
11 -<p>Binary file. <a href="/repos/{repo.owner.username}/{repo.name}/raw/{ref}/{path}">Download</a></p>
12 +<p>Binary file. <a class="btn btn-secondary" href="/repos/{repo.owner.username}/{repo.name}/raw/{ref}/{path}">Download</a></p>
12 13 {#else if language}
13 14 <pre><code class="language-{language}">{content}</code></pre>
14 15 {#else}
MODIFY src/main/resources/templates/RepositoryResource/branches.html +3 -1
diff --git a/src/main/resources/templates/RepositoryResource/branches.html b/src/main/resources/templates/RepositoryResource/branches.html
index 2b37eb3..662c7a3 100644
--- a/src/main/resources/templates/RepositoryResource/branches.html
+++ b/src/main/resources/templates/RepositoryResource/branches.html
@@ -1,13 +1,15 @@
1 1 {#include layout}
2 2 {#title}Branches – {repo.name}{/title}
3 3 <h1><a href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></h1>
4 +<p>Branches and tags</p>
5 +{#include RepositoryResource/tabs repo=repo tabRef=tabRef activeTab=activeTab /}
4 6 <h2>Branches</h2>
5 7 <table>
6 8 <tr><th>Branch</th><th></th></tr>
7 9 {#for branch in branches}
8 10 <tr>
9 11 <td><a href="/repos/{repo.owner.username}/{repo.name}/tree/{branch.name}/">{branch.name}</a></td>
10 - <td>{#if branch.defaultBranch}default{/if}</td>
12 + <td>{#if branch.defaultBranch}<span class="badge badge-default">default</span>{/if}</td>
11 13 </tr>
12 14 {/for}
13 15 </table>
MODIFY src/main/resources/templates/RepositoryResource/commits.html +5 -4
diff --git a/src/main/resources/templates/RepositoryResource/commits.html b/src/main/resources/templates/RepositoryResource/commits.html
index b2b0d1a..2b7d95e 100644
--- a/src/main/resources/templates/RepositoryResource/commits.html
+++ b/src/main/resources/templates/RepositoryResource/commits.html
@@ -1,7 +1,8 @@
1 1 {#include layout}
2 2 {#title}Commits at {ref} – {repo.name}{/title}
3 3 <h1><a href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></h1>
4 -<h2>Commits on <code>{ref}</code></h2>
4 +<p>Commits on <code>{ref}</code></p>
5 +{#include RepositoryResource/tabs repo=repo tabRef=ref activeTab=activeTab /}
5 6 <table>
6 7 <tr><th>Commit</th><th>Message</th><th>Author</th><th>Date</th></tr>
7 8 {#for commit in commits}
@@ -13,12 +14,12 @@
13 14 </tr>
14 15 {/for}
15 16 </table>
16 -<p>
17 +<p class="pagination">
17 18 {#if page > 0}
18 - <a href="/repos/{repo.owner.username}/{repo.name}/commits/{ref}?page={prevPage}&size={size}">Newer</a>
19 + <a class="btn btn-secondary" href="/repos/{repo.owner.username}/{repo.name}/commits/{ref}?page={prevPage}&size={size}">Newer</a>
19 20 {/if}
20 21 {#if hasNext}
21 - <a href="/repos/{repo.owner.username}/{repo.name}/commits/{ref}?page={nextPage}&size={size}">Older</a>
22 + <a class="btn btn-secondary" href="/repos/{repo.owner.username}/{repo.name}/commits/{ref}?page={nextPage}&size={size}">Older</a>
22 23 {/if}
23 24 </p>
24 25 {/include}
ADD src/main/resources/templates/RepositoryResource/tabs.html +7 -0
diff --git a/src/main/resources/templates/RepositoryResource/tabs.html b/src/main/resources/templates/RepositoryResource/tabs.html
new file mode 100644
index 0000000..f1e7446
--- /dev/null
+++ b/src/main/resources/templates/RepositoryResource/tabs.html
@@ -0,0 +1,7 @@
1 +{#if tabRef}
2 +<nav class="tabs">
3 + <a class="tab{#if activeTab == 'files'} active{/if}" href="/repos/{repo.owner.username}/{repo.name}/tree/{tabRef}/">Files</a>
4 + <a class="tab{#if activeTab == 'commits'} active{/if}" href="/repos/{repo.owner.username}/{repo.name}/commits/{tabRef}">Commits</a>
5 + <a class="tab{#if activeTab == 'branches'} active{/if}" href="/repos/{repo.owner.username}/{repo.name}/branches">Branches</a>
6 +</nav>
7 +{/if}
MODIFY src/main/resources/templates/RepositoryResource/tree.html +1 -0
diff --git a/src/main/resources/templates/RepositoryResource/tree.html b/src/main/resources/templates/RepositoryResource/tree.html
index 682bac8..fcb93fb 100644
--- a/src/main/resources/templates/RepositoryResource/tree.html
+++ b/src/main/resources/templates/RepositoryResource/tree.html
@@ -1,6 +1,7 @@
1 1 {#include layout}
2 2 {#title}{path ?: '/'} at {ref} – {repo.name}{/title}
3 3 <h1><a href="/repos/{repo.owner.username}/{repo.name}">{repo.owner.username}/{repo.name}</a></h1>
4 +{#include RepositoryResource/tabs repo=repo tabRef=ref activeTab=activeTab /}
4 5 <nav class="breadcrumb" aria-label="Path">
5 6 {#for crumb in crumbs}
6 7 {#if crumb.href}<a href="{crumb.href}">{crumb.label}</a>{#else}<span class="current">{crumb.label}</span>{/if}
MODIFY src/main/resources/templates/SettingsResource/keys.html +2 -2
diff --git a/src/main/resources/templates/SettingsResource/keys.html b/src/main/resources/templates/SettingsResource/keys.html
index 8ecac2e..d1fda60 100644
--- a/src/main/resources/templates/SettingsResource/keys.html
+++ b/src/main/resources/templates/SettingsResource/keys.html
@@ -5,12 +5,12 @@
5 5 <p class="error">{error}</p>
6 6 {/if}
7 7 <table>
8 - <tr><th>Title</th><th>Fingerprint</th><th></th></tr>
8 + <tr><th>Title</th><th>Fingerprint</th><th class="actions"></th></tr>
9 9 {#for key in keys}
10 10 <tr>
11 11 <td>{key.title}</td>
12 12 <td><code>{key.fingerprint}</code></td>
13 - <td>
13 + <td class="actions">
14 14 <form class="inline" method="post" action="/settings/keys/{key.id}/delete">
15 15 <button class="btn btn-danger btn-sm">Remove</button>
16 16 </form>
MODIFY src/main/resources/templates/SettingsResource/tokenCreated.html +2 -2
diff --git a/src/main/resources/templates/SettingsResource/tokenCreated.html b/src/main/resources/templates/SettingsResource/tokenCreated.html
index e9b39c3..493e2dd 100644
--- a/src/main/resources/templates/SettingsResource/tokenCreated.html
+++ b/src/main/resources/templates/SettingsResource/tokenCreated.html
@@ -2,7 +2,7 @@
2 2 {#title}Token created – git-shark{/title}
3 3 <h1>Token created</h1>
4 4 <p>Copy your new access token now — it will not be shown again:</p>
5 -<p><code>{plaintext}</code></p>
5 +<pre><code>{plaintext}</code></pre>
6 6 <p>Use it as the password for Git over HTTPS.</p>
7 -<p><a href="/settings/tokens">Back to tokens</a></p>
7 +<p><a class="btn btn-secondary" href="/settings/tokens">Back to tokens</a></p>
8 8 {/include}
MODIFY src/main/resources/templates/SettingsResource/tokens.html +2 -2
diff --git a/src/main/resources/templates/SettingsResource/tokens.html b/src/main/resources/templates/SettingsResource/tokens.html
index 70f1886..b4976d4 100644
--- a/src/main/resources/templates/SettingsResource/tokens.html
+++ b/src/main/resources/templates/SettingsResource/tokens.html
@@ -2,13 +2,13 @@
2 2 {#title}Access tokens – git-shark{/title}
3 3 <h1>Access tokens</h1>
4 4 <table>
5 - <tr><th>Label</th><th>Created</th><th>Last used</th><th></th></tr>
5 + <tr><th>Label</th><th>Created</th><th>Last used</th><th class="actions"></th></tr>
6 6 {#for token in tokens}
7 7 <tr>
8 8 <td>{token.label}</td>
9 9 <td>{token.createdAt}</td>
10 10 <td>{token.lastUsed ?: 'never'}</td>
11 - <td>
11 + <td class="actions">
12 12 <form class="inline" method="post" action="/settings/tokens/{token.id}/revoke">
13 13 <button class="btn btn-danger btn-sm">Revoke</button>
14 14 </form>
MODIFY src/main/resources/templates/layout.html +0 -3
diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html
index 3f8b4ca..488e351 100644
--- a/src/main/resources/templates/layout.html
+++ b/src/main/resources/templates/layout.html
@@ -5,9 +5,6 @@
5 5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 6 <title>{#insert title}git-shark{/}</title>
7 7 <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8 - <link rel="preconnect" href="https://fonts.googleapis.com">
9 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10 - <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
11 8 <link rel="stylesheet" href="/shark.css">
12 9 <script defer src="/shark-hotkeys.js"></script>
13 10 </head>
MODIFY src/test/java/de/workaround/SmokeIT.java +15 -0
diff --git a/src/test/java/de/workaround/SmokeIT.java b/src/test/java/de/workaround/SmokeIT.java
index 4d674c2..6d1e6ca 100644
--- a/src/test/java/de/workaround/SmokeIT.java
+++ b/src/test/java/de/workaround/SmokeIT.java
@@ -52,6 +52,21 @@
52 52 }
53 53
54 54 @Test
55 + void designSystemAssetsAreServed()
56 + {
57 + given()
58 + .when().get("/shark.css")
59 + .then()
60 + .statusCode(200)
61 + .body(org.hamcrest.CoreMatchers.containsString(":root"));
62 +
63 + given()
64 + .when().get("/shark-hotkeys.js")
65 + .then()
66 + .statusCode(200);
67 + }
68 +
69 + @Test
55 70 void sshPortPresentsSshBanner() throws Exception
56 71 {
57 72 int port = Integer.getInteger("gitshark.ssh.port", 2222);
MODIFY src/test/java/de/workaround/web/DesignSystemTest.java +70 -0
diff --git a/src/test/java/de/workaround/web/DesignSystemTest.java b/src/test/java/de/workaround/web/DesignSystemTest.java
index 3d83e95..6ee1442 100644
--- a/src/test/java/de/workaround/web/DesignSystemTest.java
+++ b/src/test/java/de/workaround/web/DesignSystemTest.java
@@ -78,6 +78,76 @@
78 78 .body(containsString("/shark-hotkeys.js"));
79 79 }
80 80
81 + @Test
82 + void branchesPageMarksDefaultBranchWithBadge() throws Exception
83 + {
84 + User owner = persistUser("ds-badge-" + unique());
85 + Repository repo = service.create(owner, "ds-branched", Repository.Visibility.PUBLIC, null);
86 + de.workaround.git.GitTestSeeder.seed(service.repositoryPath(repo),
87 + java.util.Map.of("a.txt", "a\n".getBytes(java.nio.charset.StandardCharsets.UTF_8)));
88 +
89 + given().when().get("/repos/" + owner.username + "/ds-branched/branches")
90 + .then().statusCode(200)
91 + .body(containsString("badge badge-default"));
92 + }
93 +
94 + @Test
95 + void commitPaginationUsesSecondaryButtons() throws Exception
96 + {
97 + User owner = persistUser("ds-pager-" + unique());
98 + Repository repo = service.create(owner, "ds-paged", Repository.Visibility.PUBLIC, null);
99 + de.workaround.git.GitTestSeeder.seed(service.repositoryPath(repo),
100 + java.util.Map.of("a.txt", "a\n".getBytes(java.nio.charset.StandardCharsets.UTF_8)), 3);
101 +
102 + String base = "/repos/" + owner.username + "/ds-paged/commits/main";
103 +
104 + given().when().get(base + "?size=2")
105 + .then().statusCode(200)
106 + .body(containsString("class=\"btn btn-secondary\""))
107 + .body(containsString("Older"));
108 +
109 + given().when().get(base + "?size=2&page=1")
110 + .then().statusCode(200)
111 + .body(containsString("class=\"btn btn-secondary\""))
112 + .body(containsString("Newer"));
113 + }
114 +
115 + @Test
116 + @io.quarkus.test.security.TestSecurity(user = "ds-settings")
117 + void settingsPagesUseDesignSystemActionColumns()
118 + {
119 + persistUser("ds-settings");
120 +
121 + given().when().get("/settings/keys")
122 + .then().statusCode(200)
123 + .body(containsString("class=\"btn btn-primary\""))
124 + .body(containsString("class=\"actions\""));
125 +
126 + given().when().get("/settings/tokens")
127 + .then().statusCode(200)
128 + .body(containsString("class=\"btn btn-primary\""))
129 + .body(containsString("class=\"actions\""));
130 + }
131 +
132 + @Test
133 + void fontsAreSelfHostedWithoutCdnRequests()
134 + {
135 + given().when().get("/")
136 + .then().statusCode(200)
137 + .body(not(containsString("fonts.googleapis.com")))
138 + .body(not(containsString("fonts.gstatic.com")));
139 +
140 + given().when().get("/shark.css")
141 + .then().statusCode(200)
142 + .body(containsString("@font-face"))
143 + .body(containsString("/fonts/"));
144 +
145 + given().when().get("/fonts/space-grotesk-latin.woff2")
146 + .then().statusCode(200);
147 + given().when().get("/fonts/jetbrains-mono-latin.woff2")
148 + .then().statusCode(200);
149 + }
150 +
81 151 private static String unique()
82 152 {
83 153 return UUID.randomUUID().toString().substring(0, 8);
MODIFY src/test/java/de/workaround/web/WebUiTest.java +74 -0
diff --git a/src/test/java/de/workaround/web/WebUiTest.java b/src/test/java/de/workaround/web/WebUiTest.java
index 2fd5fc1..ecd590e 100644
--- a/src/test/java/de/workaround/web/WebUiTest.java
+++ b/src/test/java/de/workaround/web/WebUiTest.java
@@ -268,6 +268,80 @@
268 268 }
269 269
270 270 @Test
271 + void repositoryPagesRenderTabsWithActiveSection() throws Exception
272 + {
273 + User owner = persistUser("ui-tabs-" + unique());
274 + Repository repo = service.create(owner, "tabbed", Repository.Visibility.PUBLIC, null);
275 + GitTestSeeder.seed(service.repositoryPath(repo),
276 + Map.of("a.txt", "a\n".getBytes(StandardCharsets.UTF_8)));
277 +
278 + String base = "/repos/" + owner.username + "/tabbed";
279 +
280 + given().when().get(base + "/tree/main")
281 + .then().statusCode(200)
282 + .body(containsString("class=\"tabs\""))
283 + .body(containsString("class=\"tab active\" href=\"" + base + "/tree/main/\">Files"))
284 + .body(containsString("class=\"tab\" href=\"" + base + "/commits/main\">Commits"))
285 + .body(containsString("class=\"tab\" href=\"" + base + "/branches\">Branches"));
286 +
287 + given().when().get(base + "/tree/main/a.txt")
288 + .then().statusCode(200)
289 + .body(containsString("class=\"tab active\" href=\"" + base + "/tree/main/\">Files"));
290 +
291 + given().when().get(base + "/commits/main")
292 + .then().statusCode(200)
293 + .body(containsString("class=\"tab active\" href=\"" + base + "/commits/main\">Commits"));
294 +
295 + given().when().get(base + "/branches")
296 + .then().statusCode(200)
297 + .body(containsString("class=\"tab active\" href=\"" + base + "/branches\">Branches"));
298 + }
299 +
300 + @Test
301 + void tabLinksPreserveSelectedRef() throws Exception
302 + {
303 + User owner = persistUser("ui-kate-" + unique());
304 + Repository repo = service.create(owner, "refkeep", Repository.Visibility.PUBLIC, null);
305 + GitTestSeeder.seed(service.repositoryPath(repo),
306 + Map.of("a.txt", "a\n".getBytes(StandardCharsets.UTF_8)));
307 + try (org.eclipse.jgit.api.Git git = org.eclipse.jgit.api.Git.open(service.repositoryPath(repo).toFile()))
308 + {
309 + git.branchCreate().setName("feature").setStartPoint("main").call();
310 + }
311 +
312 + String base = "/repos/" + owner.username + "/refkeep";
313 + given().when().get(base + "/tree/feature")
314 + .then().statusCode(200)
315 + .body(containsString("class=\"tab active\" href=\"" + base + "/tree/feature/\">Files"))
316 + .body(containsString("href=\"" + base + "/commits/feature\">Commits"));
317 + }
318 +
319 + @Test
320 + @io.quarkus.test.security.TestSecurity(user = "ui-nojs")
321 + void formsWorkWithoutJavaScriptViaStandardPost() throws Exception
322 + {
323 + persistUser("ui-nojs");
324 + String name = "nojs-" + unique();
325 +
326 + // a plain form-encoded POST (what a browser sends with JS disabled) must complete the flow
327 + given().contentType("application/x-www-form-urlencoded")
328 + .formParam("name", name)
329 + .formParam("visibility", "PUBLIC")
330 + .formParam("description", "created without JS")
331 + .when().redirects().follow(false).post("/repos")
332 + .then().statusCode(org.hamcrest.Matchers.anyOf(
333 + org.hamcrest.Matchers.is(302), org.hamcrest.Matchers.is(303)));
334 +
335 + given().when().get("/repos/ui-nojs/" + name)
336 + .then().statusCode(200);
337 +
338 + // scripting is enhancement-only: loaded deferred, no inline handlers required for rendering
339 + given().when().get("/")
340 + .then().statusCode(200)
341 + .body(containsString("<script defer src=\"/shark-hotkeys.js\">"));
342 + }
343 +
344 + @Test
271 345 void branchesPageMarksDefaultBranch() throws Exception
272 346 {
273 347 User owner = persistUser("ui-fred-" + unique());

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog