💄 fix: move clone into a dialog on repo overview
Changes
6 files changed, +604 -59
MODIFY
src/main/java/de/workaround/git/GitBrowseService.java
+23 -0
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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> · <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> ·
26
- <a href="/repos/{repo.owner.username}/{repo.name}/branches">Branches & 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}📁{/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
@@ -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
{