gitshark

Clone repository

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

← Commits

💄 fix: move clone into a dialog on repo overview

bdf99a5ef78c95608bdc673d7b66bb2eccf77960 · Michael Hainz · 2026-06-26T12:57:55Z

Changes

6 files changed, +604 -59

MODIFY src/main/java/de/workaround/git/GitBrowseService.java +23 -0
diff --git a/src/main/java/de/workaround/git/GitBrowseService.java b/src/main/java/de/workaround/git/GitBrowseService.java
index ed94427..5012095 100644
--- a/src/main/java/de/workaround/git/GitBrowseService.java
+++ b/src/main/java/de/workaround/git/GitBrowseService.java
@@ -199,6 +199,29 @@
199 199 }
200 200 }
201 201
202 + public int commitCount(Path barePath, String ref)
203 + {
204 + try (Repository repo = open(barePath); RevWalk revWalk = new RevWalk(repo))
205 + {
206 + RevCommit start = resolveCommit(repo, revWalk, ref);
207 + if (start == null)
208 + {
209 + return 0;
210 + }
211 + revWalk.markStart(start);
212 + int count = 0;
213 + for (RevCommit ignored : revWalk)
214 + {
215 + count++;
216 + }
217 + return count;
218 + }
219 + catch (IOException e)
220 + {
221 + throw new UncheckedIOException(e);
222 + }
223 + }
224 +
202 225 public List<BranchInfo> branches(Path barePath)
203 226 {
204 227 String defaultBranch = defaultBranch(barePath);
MODIFY src/main/java/de/workaround/web/RepositoryResource.java +52 -2
diff --git a/src/main/java/de/workaround/web/RepositoryResource.java b/src/main/java/de/workaround/web/RepositoryResource.java
index c7492b2..54b7c1c 100644
--- a/src/main/java/de/workaround/web/RepositoryResource.java
+++ b/src/main/java/de/workaround/web/RepositoryResource.java
@@ -3,6 +3,8 @@
3 3 import java.net.URI;
4 4 import java.nio.charset.StandardCharsets;
5 5 import java.nio.file.Path;
6 +import java.time.Duration;
7 +import java.time.Instant;
6 8 import java.util.List;
7 9
8 10 import de.workaround.account.CurrentUser;
@@ -39,7 +41,8 @@
39 41 {
40 42 static native TemplateInstance overview(Repository repo, boolean owner, boolean empty, String defaultBranch,
41 43 List<GitBrowseService.TreeEntry> entries, String httpUrl, String sshUrl, boolean loggedIn,
42 - boolean pinned);
44 + boolean pinned, GitBrowseService.CommitInfo latestCommit, String latestCommitAge, int commitCount,
45 + int branchCount, int tagCount);
43 46
44 47 static native TemplateInstance tree(Repository repo, String ref, String path,
45 48 List<GitBrowseService.TreeEntry> entries);
@@ -88,8 +91,55 @@
88 91 boolean isOwner = user != null && user.id.equals(repo.owner.id);
89 92 boolean loggedIn = user != null;
90 93 boolean pinned = loggedIn && pinService.isPinned(user, repo);
94 + GitBrowseService.CommitInfo latestCommit = empty ? null
95 + : browse.commits(path, defaultBranch, 0, 1)
96 + .filter(commitPage -> !commitPage.commits().isEmpty())
97 + .map(commitPage -> commitPage.commits().get(0))
98 + .orElse(null);
99 + String latestCommitAge = latestCommit == null ? null : relativeAge(latestCommit.date());
100 + int commitCount = empty ? 0 : browse.commitCount(path, defaultBranch);
101 + int branchCount = browse.branches(path).size();
102 + int tagCount = browse.tags(path).size();
91 103 return Templates.overview(repo, isOwner, empty, defaultBranch, entries, httpUrl(repo), sshUrl(repo),
92 - loggedIn, pinned);
104 + loggedIn, pinned, latestCommit, latestCommitAge, commitCount, branchCount, tagCount);
105 + }
106 +
107 + private static String relativeAge(Instant when)
108 + {
109 + Duration elapsed = Duration.between(when, Instant.now());
110 + if (elapsed.isNegative())
111 + {
112 + elapsed = Duration.ZERO;
113 + }
114 + long days = elapsed.toDays();
115 + if (days >= 365)
116 + {
117 + return plural(days / 365, "year");
118 + }
119 + if (days >= 30)
120 + {
121 + return plural(days / 30, "month");
122 + }
123 + if (days >= 1)
124 + {
125 + return plural(days, "day");
126 + }
127 + long hours = elapsed.toHours();
128 + if (hours >= 1)
129 + {
130 + return plural(hours, "hour");
131 + }
132 + long minutes = elapsed.toMinutes();
133 + if (minutes >= 1)
134 + {
135 + return plural(minutes, "minute");
136 + }
137 + return "just now";
138 + }
139 +
140 + private static String plural(long count, String unit)
141 + {
142 + return count + " " + unit + (count == 1 ? "" : "s") + " ago";
93 143 }
94 144
95 145 @GET
MODIFY src/main/resources/META-INF/resources/shark-hotkeys.js +13 -0
diff --git a/src/main/resources/META-INF/resources/shark-hotkeys.js b/src/main/resources/META-INF/resources/shark-hotkeys.js
index ed03fce..7f35aa0 100644
--- a/src/main/resources/META-INF/resources/shark-hotkeys.js
+++ b/src/main/resources/META-INF/resources/shark-hotkeys.js
@@ -62,4 +62,17 @@
62 62 dialog.showModal();
63 63 }
64 64 });
65 +
66 + // Generic dialog opener: [data-open-dialog="id"] shows the matching <dialog> as a modal.
67 + document.addEventListener("click", function (event) {
68 + var trigger = event.target.closest("[data-open-dialog]");
69 + if (!trigger) {
70 + return;
71 + }
72 + var target = document.getElementById(trigger.getAttribute("data-open-dialog"));
73 + if (target && typeof target.showModal === "function") {
74 + event.preventDefault();
75 + target.showModal();
76 + }
77 + });
65 78 })();
MODIFY src/main/resources/META-INF/resources/shark.css +383 -14
diff --git a/src/main/resources/META-INF/resources/shark.css b/src/main/resources/META-INF/resources/shark.css
index bc7720e..e9407ba 100644
--- a/src/main/resources/META-INF/resources/shark.css
+++ b/src/main/resources/META-INF/resources/shark.css
@@ -613,20 +613,6 @@
613 613 border-color: var(--accent);
614 614 }
615 615
616 -/* clone command pill */
617 -
618 -.clonecmd {
619 - display: inline-flex;
620 - align-items: center;
621 - gap: 10px;
622 - font: 500 12.5px/1 var(--mono);
623 - color: var(--muted);
624 - background: var(--surface);
625 - border: 1px solid var(--border);
626 - border-radius: 999px;
627 - padding: 8px 14px;
628 -}
629 -
630 616 /* dashboard */
631 617
632 618 .dashboard-section {
@@ -705,3 +691,386 @@
705 691 border-radius: 5px;
706 692 padding: 2px 6px;
707 693 }
694 +
695 +/* repo overview — two-column sidebar + main */
696 +
697 +.repo-layout {
698 + display: grid;
699 + grid-template-columns: 256px 1fr;
700 + gap: 40px;
701 +}
702 +
703 +.repo-side .owner {
704 + font: 500 13px/1 var(--mono);
705 + color: var(--muted);
706 +}
707 +
708 +.repo-side .repo-name {
709 + margin: 6px 0 12px;
710 + font-size: 26px;
711 + font-weight: 700;
712 + letter-spacing: -0.03em;
713 + line-height: 1.1;
714 + color: var(--ink);
715 +}
716 +
717 +.repo-side .tag {
718 + display: inline-flex;
719 + align-items: center;
720 + gap: 6px;
721 + font: 500 11px/1 var(--mono);
722 + text-transform: uppercase;
723 + letter-spacing: .06em;
724 + color: var(--accent-deep);
725 + background: var(--accent-soft);
726 + border-radius: 999px;
727 + padding: 5px 10px;
728 +}
729 +
730 +.repo-side .tag .dot {
731 + width: 6px;
732 + height: 6px;
733 + border-radius: 50%;
734 + background: var(--accent);
735 +}
736 +
737 +.repo-side .tag.tag-private {
738 + color: var(--attention);
739 + background: var(--attention-bg);
740 +}
741 +
742 +.repo-side .tag.tag-private .dot {
743 + background: var(--attention);
744 +}
745 +
746 +.repo-side .desc {
747 + margin: 16px 0 14px;
748 + color: var(--ink);
749 + font-size: 14px;
750 + line-height: 1.6;
751 +}
752 +
753 +.repo-side .rule {
754 + height: 1px;
755 + background: var(--border-soft);
756 + margin: 18px 0;
757 +}
758 +
759 +.repo-nav {
760 + display: flex;
761 + flex-direction: column;
762 + gap: 2px;
763 +}
764 +
765 +.repo-nav a {
766 + display: flex;
767 + align-items: center;
768 + gap: 11px;
769 + padding: 9px 12px;
770 + border-radius: var(--radius-sm);
771 + color: var(--muted);
772 + font-weight: 500;
773 + font-size: 14px;
774 + position: relative;
775 + transition: background .12s, color .12s;
776 +}
777 +
778 +.repo-nav a:hover {
779 + background: var(--surface);
780 + color: var(--ink);
781 + text-decoration: none;
782 +}
783 +
784 +.repo-nav a .g {
785 + width: 18px;
786 + text-align: center;
787 + color: var(--faint);
788 + font-size: 15px;
789 +}
790 +
791 +.repo-nav a .ct {
792 + margin-left: auto;
793 + font: 500 12px/1 var(--mono);
794 + color: var(--faint);
795 +}
796 +
797 +.repo-nav a.active {
798 + background: var(--surface);
799 + color: var(--ink);
800 + font-weight: 600;
801 + box-shadow: 0 0 0 1px var(--border);
802 +}
803 +
804 +.repo-nav a.active::before {
805 + content: "";
806 + position: absolute;
807 + left: 0;
808 + top: 9px;
809 + bottom: 9px;
810 + width: 3px;
811 + border-radius: 3px;
812 + background: var(--accent);
813 +}
814 +
815 +.repo-nav a.active .g {
816 + color: var(--accent);
817 +}
818 +
819 +.repo-actions {
820 + display: flex;
821 + align-items: center;
822 + gap: 8px;
823 +}
824 +
825 +.repo-actions .btn {
826 + flex: 1;
827 + justify-content: center;
828 +}
829 +
830 +.btn-clone {
831 + flex: 1;
832 + display: inline-flex;
833 + align-items: center;
834 + justify-content: center;
835 + gap: 8px;
836 + font: 600 13px/1 var(--font);
837 + color: #fff;
838 + background: var(--accent);
839 + border: 1px solid var(--accent);
840 + border-radius: var(--radius);
841 + padding: 9px 14px;
842 + cursor: pointer;
843 +}
844 +
845 +.btn-clone:hover {
846 + background: var(--accent-deep);
847 + border-color: var(--accent-deep);
848 +}
849 +
850 +.btn-clone .g {
851 + font: 500 14px/1 var(--mono);
852 +}
853 +
854 +/* clone dialog */
855 +
856 +.clone-dialog {
857 + border: 1px solid var(--border);
858 + border-radius: var(--radius);
859 + background: var(--surface);
860 + color: var(--ink);
861 + padding: var(--s4);
862 + width: min(440px, 92vw);
863 + box-shadow: 0 20px 60px rgba(0, 0, 0, .25);
864 +}
865 +
866 +.clone-dialog::backdrop {
867 + background: rgba(0, 0, 0, .45);
868 +}
869 +
870 +.clone-dialog h2 {
871 + margin: 0 0 var(--s3);
872 + font-size: 18px;
873 +}
874 +
875 +.clone-close {
876 + float: right;
877 + margin: -4px -4px 0 0;
878 +}
879 +
880 +.clone-tabs {
881 + display: flex;
882 + gap: 6px;
883 + margin-bottom: var(--s2);
884 +}
885 +
886 +.clone-dialog > input[type="radio"] {
887 + position: absolute;
888 + opacity: 0;
889 + pointer-events: none;
890 +}
891 +
892 +.clone-tabs label {
893 + font: 600 12px/1 var(--mono);
894 + color: var(--muted);
895 + border: 1px solid var(--border);
896 + border-radius: 999px;
897 + padding: 7px 16px;
898 + cursor: pointer;
899 +}
900 +
901 +#clone-https:checked ~ .clone-tabs label[for="clone-https"],
902 +#clone-ssh:checked ~ .clone-tabs label[for="clone-ssh"] {
903 + color: #fff;
904 + background: var(--accent);
905 + border-color: var(--accent);
906 +}
907 +
908 +.clone-url {
909 + display: none;
910 + overflow-x: auto;
911 + white-space: nowrap;
912 + font: 500 12.5px/1.4 var(--mono);
913 + background: var(--canvas);
914 + border: 1px solid var(--border);
915 + border-radius: var(--radius);
916 + padding: 12px 14px;
917 +}
918 +
919 +#clone-https:checked ~ .url-https,
920 +#clone-ssh:checked ~ .url-ssh {
921 + display: block;
922 +}
923 +
924 +/* main column */
925 +
926 +.branchbar {
927 + display: flex;
928 + align-items: center;
929 + gap: 12px;
930 + margin-bottom: 16px;
931 +}
932 +
933 +.branchsel {
934 + display: inline-flex;
935 + align-items: center;
936 + gap: 8px;
937 + font: 600 14px/1 var(--font);
938 + color: var(--ink);
939 + background: var(--surface);
940 + border: 1px solid var(--border);
941 + border-radius: 999px;
942 + padding: 9px 14px;
943 +}
944 +
945 +.branchsel:hover {
946 + text-decoration: none;
947 + border-color: var(--border-strong);
948 +}
949 +
950 +.branchsel .g {
951 + color: var(--accent);
952 +}
953 +
954 +.branchsel .car {
955 + color: var(--faint);
956 + font-size: 11px;
957 +}
958 +
959 +.branchbar .meta {
960 + font: 500 13px/1 var(--mono);
961 + color: var(--muted);
962 +}
963 +
964 +.branchbar .meta b {
965 + color: var(--ink);
966 + font-weight: 600;
967 +}
968 +
969 +.commitrow {
970 + display: flex;
971 + align-items: center;
972 + gap: 12px;
973 + padding: 14px 18px;
974 + background: var(--accent-soft);
975 + border-bottom: 1px solid var(--border);
976 + color: var(--ink);
977 +}
978 +
979 +.commitrow:hover {
980 + text-decoration: none;
981 +}
982 +
983 +.commitrow .av {
984 + width: 26px;
985 + height: 26px;
986 + border-radius: 8px;
987 + background: var(--accent);
988 + color: #fff;
989 + display: flex;
990 + align-items: center;
991 + justify-content: center;
992 + font: 700 12px/1 var(--font);
993 + text-transform: uppercase;
994 + flex: none;
995 +}
996 +
997 +.commitrow .who {
998 + font-weight: 600;
999 +}
1000 +
1001 +.commitrow .msg {
1002 + color: var(--ink);
1003 + overflow: hidden;
1004 + text-overflow: ellipsis;
1005 + white-space: nowrap;
1006 +}
1007 +
1008 +.commitrow .hash {
1009 + margin-left: auto;
1010 + font: 500 12.5px/1 var(--mono);
1011 + color: var(--muted);
1012 + white-space: nowrap;
1013 +}
1014 +
1015 +.files {
1016 + display: flex;
1017 + flex-direction: column;
1018 +}
1019 +
1020 +.frow {
1021 + display: flex;
1022 + align-items: center;
1023 + padding: 12px 18px;
1024 + border-bottom: 1px solid var(--border-soft);
1025 + color: inherit;
1026 + transition: background .1s;
1027 +}
1028 +
1029 +.frow:last-child {
1030 + border-bottom: none;
1031 +}
1032 +
1033 +.frow:hover {
1034 + background: var(--accent-soft);
1035 + text-decoration: none;
1036 +}
1037 +
1038 +.frow .fname {
1039 + display: flex;
1040 + align-items: center;
1041 + gap: 11px;
1042 + min-width: 0;
1043 +}
1044 +
1045 +.frow .fname .g {
1046 + width: 18px;
1047 + text-align: center;
1048 + font-size: 15px;
1049 + color: var(--faint);
1050 + flex: none;
1051 +}
1052 +
1053 +.frow .fname .g.dir {
1054 + color: var(--accent);
1055 +}
1056 +
1057 +.frow .fname .n {
1058 + font: 500 14px/1 var(--mono);
1059 + color: var(--ink);
1060 + white-space: nowrap;
1061 +}
1062 +
1063 +.frow:hover .fname .n {
1064 + color: var(--accent-deep);
1065 +}
1066 +
1067 +.danger-zone {
1068 + margin-top: var(--s6);
1069 +}
1070 +
1071 +@media (max-width: 760px) {
1072 + .repo-layout {
1073 + grid-template-columns: 1fr;
1074 + gap: 28px;
1075 + }
1076 +}
MODIFY src/main/resources/templates/RepositoryResource/overview.html +94 -43
diff --git a/src/main/resources/templates/RepositoryResource/overview.html b/src/main/resources/templates/RepositoryResource/overview.html
index fce4088..92d533e 100644
--- a/src/main/resources/templates/RepositoryResource/overview.html
+++ b/src/main/resources/templates/RepositoryResource/overview.html
@@ -1,47 +1,98 @@
1 1 {#include layout}
2 2 {#title}{repo.owner.username}/{repo.name} – git-shark{/title}
3 -<h1>{repo.owner.username}/{repo.name} <small>({repo.visibility})</small></h1>
4 -{#if repo.description}
5 -<p>{repo.description}</p>
6 -{/if}
7 -<p>
8 - Clone: <code>{httpUrl}</code> &middot; <code>{sshUrl}</code>
9 -</p>
10 -{#if loggedIn}
11 -<form class="inline" method="post" action="/repos/{repo.owner.username}/{repo.name}/{#if pinned}unpin{#else}pin{/if}">
12 - <input type="hidden" name="redirect" value="/repos/{repo.owner.username}/{repo.name}">
13 - <button class="btn-icon{#if pinned} pinned{/if}" type="submit" title="{#if pinned}Unpin{#else}Pin{/if} repository" aria-label="{#if pinned}Unpin{#else}Pin{/if} repository">
14 - <svg viewBox="0 0 24 24" fill="{#if pinned}currentColor{#else}none{/if}" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg>
15 - </button>
16 -</form>
17 -{/if}
18 -{#if empty}
19 -<h2>Quick start</h2>
20 -<p>This repository is empty. Push an existing repository:</p>
21 -<pre>git remote add origin {httpUrl}
3 +
4 +<div class="repo-layout">
5 +
6 + <aside class="repo-side">
7 + <div class="owner">{repo.owner.username} /</div>
8 + <div class="repo-name">{repo.name}</div>
9 + <span class="tag{#if repo.visibility.name() == 'PRIVATE'} tag-private{/if}"><span class="dot"></span> {repo.visibility.name()}</span>
10 + {#if repo.description}
11 + <p class="desc">{repo.description}</p>
12 + {/if}
13 + <div class="repo-actions">
14 + <button type="button" class="btn-clone" data-open-dialog="clone-dialog">
15 + <span class="g">⤓</span> Clone
16 + </button>
17 + {#if loggedIn}
18 + <form class="inline" method="post" action="/repos/{repo.owner.username}/{repo.name}/{#if pinned}unpin{#else}pin{/if}">
19 + <input type="hidden" name="redirect" value="/repos/{repo.owner.username}/{repo.name}">
20 + <button class="btn-icon{#if pinned} pinned{/if}" type="submit" title="{#if pinned}Unpin{#else}Pin{/if} repository" aria-label="{#if pinned}Unpin{#else}Pin{/if} repository">
21 + <svg viewBox="0 0 24 24" fill="{#if pinned}currentColor{#else}none{/if}" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg>
22 + </button>
23 + </form>
24 + {/if}
25 + </div>
26 + <div class="rule"></div>
27 + <nav class="repo-nav">
28 + <a class="active" href="/repos/{repo.owner.username}/{repo.name}"><span class="g">⌗</span> Code</a>
29 + {#if !empty}
30 + <a href="/repos/{repo.owner.username}/{repo.name}/commits/{defaultBranch}"><span class="g">â—´</span> Commits <span class="ct">{commitCount}</span></a>
31 + {/if}
32 + <a href="/repos/{repo.owner.username}/{repo.name}/branches"><span class="g">â‘‚</span> Branches <span class="ct">{branchCount}</span></a>
33 + <a href="/repos/{repo.owner.username}/{repo.name}/branches"><span class="g">⬡</span> Tags <span class="ct">{tagCount}</span></a>
34 + </nav>
35 + </aside>
36 +
37 + <section class="repo-main">
38 + {#if empty}
39 + <h2>Quick start</h2>
40 + <p>This repository is empty. Push an existing repository:</p>
41 + <pre>git remote add origin {httpUrl}
22 42 git push -u origin main</pre>
23 -{#else}
24 -<p>
25 - <a href="/repos/{repo.owner.username}/{repo.name}/commits/{defaultBranch}">Commits</a> &middot;
26 - <a href="/repos/{repo.owner.username}/{repo.name}/branches">Branches &amp; tags</a>
27 -</p>
28 -<table>
29 - <tr><th>Name</th></tr>
30 - {#for entry in entries}
31 - <tr>
32 - <td>
33 - {#if entry.directory}&#x1F4C1;{/if}
34 - <a href="/repos/{repo.owner.username}/{repo.name}/tree/{defaultBranch}/{entry.path}">{entry.name}</a>
35 - </td>
36 - </tr>
37 - {/for}
38 -</table>
39 -{/if}
40 -{#if owner}
41 -<h2>Danger zone</h2>
42 -<form method="post" action="/repos/{repo.owner.username}/{repo.name}/delete">
43 - <p><label>Type the repository name to confirm deletion <input name="confirm"></label></p>
44 - <button class="btn btn-danger">Delete repository</button>
45 -</form>
46 -{/if}
43 + {#else}
44 + <div class="branchbar">
45 + <a class="branchsel" href="/repos/{repo.owner.username}/{repo.name}/branches"><span class="g">â‘‚</span> {defaultBranch} <span class="car">â–¾</span></a>
46 + <span class="meta"><b>{commitCount}</b> commits · <b>{entries.size()}</b> entries</span>
47 + </div>
48 +
49 + <div class="panel">
50 + {#if latestCommit}
51 + <a class="commitrow" href="/repos/{repo.owner.username}/{repo.name}/commits/{defaultBranch}">
52 + <span class="av">{latestCommit.author.charAt(0)}</span>
53 + <span class="who">{latestCommit.author}</span>
54 + <span class="msg">{latestCommit.message}</span>
55 + <span class="hash">{latestCommit.shortId} · {latestCommitAge}</span>
56 + </a>
57 + {/if}
58 + <div class="files">
59 + {#for entry in entries}
60 + <a class="frow" href="/repos/{repo.owner.username}/{repo.name}/tree/{defaultBranch}/{entry.path}">
61 + <span class="fname">
62 + <span class="g{#if entry.directory} dir{/if}">{#if entry.directory}▸{#else}≡{/if}</span>
63 + <span class="n">{entry.name}</span>
64 + </span>
65 + </a>
66 + {/for}
67 + </div>
68 + </div>
69 + {/if}
70 +
71 + {#if owner}
72 + <div class="danger-zone">
73 + <h2>Danger zone</h2>
74 + <form method="post" action="/repos/{repo.owner.username}/{repo.name}/delete">
75 + <p><label>Type the repository name to confirm deletion <input name="confirm"></label></p>
76 + <button class="btn btn-danger">Delete repository</button>
77 + </form>
78 + </div>
79 + {/if}
80 + </section>
81 +
82 +</div>
83 +
84 +<dialog id="clone-dialog" class="clone-dialog">
85 + <form method="dialog" class="clone-close">
86 + <button class="btn-icon" aria-label="Close">✕</button>
87 + </form>
88 + <h2>Clone repository</h2>
89 + <input type="radio" name="clone-proto" id="clone-https" checked>
90 + <input type="radio" name="clone-proto" id="clone-ssh">
91 + <div class="clone-tabs">
92 + <label for="clone-https">HTTPS</label>
93 + <label for="clone-ssh">SSH</label>
94 + </div>
95 + <code class="clone-url url-https">git clone {httpUrl}</code>
96 + <code class="clone-url url-ssh">git clone {sshUrl}</code>
97 +</dialog>
47 98 {/include}
MODIFY src/test/java/de/workaround/web/WebUiTest.java +39 -0
diff --git a/src/test/java/de/workaround/web/WebUiTest.java b/src/test/java/de/workaround/web/WebUiTest.java
index 0b16e17..413cae2 100644
--- a/src/test/java/de/workaround/web/WebUiTest.java
+++ b/src/test/java/de/workaround/web/WebUiTest.java
@@ -188,6 +188,45 @@
188 188 }
189 189
190 190 @Test
191 + void repositoryOverviewShowsSidebarLatestCommitAndCounts() throws Exception
192 + {
193 + User owner = persistUser("ui-iris-" + unique());
194 + Repository repo = service.create(owner, "overview", Repository.Visibility.PUBLIC, "demo repo");
195 + GitTestSeeder.seed(service.repositoryPath(repo),
196 + Map.of("a.txt", "a\n".getBytes(StandardCharsets.UTF_8)), 3);
197 +
198 + given().when().get("/repos/" + owner.username + "/overview")
199 + .then().statusCode(200)
200 + .body(containsString("class=\"repo-side\""))
201 + .body(containsString("class=\"repo-nav\""))
202 + .body(containsString("class=\"commitrow\""))
203 + .body(containsString("commit 3"))
204 + .body(containsString("<b>3</b> commits"));
205 + }
206 +
207 + @Test
208 + void repositoryOverviewClonesViaDialogNotHeaderPill() throws Exception
209 + {
210 + User owner = persistUser("ui-jane-" + unique());
211 + Repository repo = service.create(owner, "clonedialog", Repository.Visibility.PUBLIC, "demo repo");
212 + GitTestSeeder.seed(service.repositoryPath(repo),
213 + Map.of("a.txt", "a\n".getBytes(StandardCharsets.UTF_8)));
214 +
215 + given().when().get("/repos/" + owner.username + "/clonedialog")
216 + .then().statusCode(200)
217 + // clone moved into a dialog opened by a button...
218 + .body(containsString("data-open-dialog=\"clone-dialog\""))
219 + .body(containsString("id=\"clone-dialog\""))
220 + // ...with both protocols selectable...
221 + .body(containsString("git clone "))
222 + .body(containsString("/git/" + owner.username + "/clonedialog.git"))
223 + .body(containsString("ssh://git@"))
224 + // ...and no longer pinned in the header topbar or sidebar
225 + .body(not(containsString("class=\"clonecmd\"")))
226 + .body(not(containsString("class=\"clone-urls\"")));
227 + }
228 +
229 + @Test
191 230 @TestSecurity(user = "ui-mallory")
192 231 void privateRepositoryHiddenFromStranger() throws Exception
193 232 {

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog