1import { SourceControlProviderKind } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import {
5 sourceControlRepositorySelector,
6 detectSourceControlProviderFromRemoteUrl,
7 getChangeRequestTerminologyForKind,
8 isSshRemoteUrl,
9 resolveChangeRequestPresentation,
10} from "./sourceControl.ts";
11
12describe("source control presentation", () => {
13 it("uses merge request terminology for GitLab", () => {
14 expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("gitlab"))).toEqual({
15 shortLabel: "MR",
16 singular: "merge request",
17 });
18 });
19
20 it("uses pull request terminology for GitHub-compatible providers", () => {
21 expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("github"))).toEqual({
22 shortLabel: "PR",
23 singular: "pull request",
24 });
25 expect(
26 getChangeRequestTerminologyForKind(SourceControlProviderKind.make("azure-devops")),
27 ).toEqual({
28 shortLabel: "PR",
29 singular: "pull request",
30 });
31 expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("bitbucket"))).toEqual(
32 {
33 shortLabel: "PR",
34 singular: "pull request",
35 },
36 );
37 });
38
39 it("falls back to generic change request copy for unknown providers", () => {
40 expect(
41 resolveChangeRequestPresentation({
42 kind: SourceControlProviderKind.make("unknown"),
43 name: "forge",
44 baseUrl: "",
45 }),
46 ).toEqual(
47 expect.objectContaining({
48 shortName: "change request",
49 longName: "change request",
50 }),
51 );
52 });
53});
54
55describe("detectSourceControlProviderFromRemoteUrl", () => {
56 it("detects common source control hosts", () => {
57 expect(detectSourceControlProviderFromRemoteUrl("git@github.com:owner/repo.git")?.kind).toBe(
58 "github",
59 );
60 expect(
61 detectSourceControlProviderFromRemoteUrl("https://gitlab.com/group/repo.git")?.kind,
62 ).toBe("gitlab");
63 expect(
64 detectSourceControlProviderFromRemoteUrl("https://dev.azure.com/org/project/_git/repo")?.kind,
65 ).toBe("azure-devops");
66 expect(
67 detectSourceControlProviderFromRemoteUrl("git@bitbucket.org:workspace/repo.git")?.kind,
68 ).toBe("bitbucket");
69 });
70
71 it("detects Forgejo and Gitea hosts while preserving HTTP origins", () => {
72 for (const host of ["codeberg.org", "forgejo.example.test", "gitea.example.test"]) {
73 expect(detectSourceControlProviderFromRemoteUrl(`http://${host}:3000/team/repo.git`)).toEqual(
74 {
75 kind: "forgejo",
76 name: "Forgejo",
77 baseUrl: `http://${host}:3000`,
78 },
79 );
80 }
81 expect(getChangeRequestTerminologyForKind(SourceControlProviderKind.make("forgejo"))).toEqual({
82 shortLabel: "PR",
83 singular: "pull request",
84 });
85 });
86
87 it("detects Azure DevOps SSH remotes", () => {
88 // The default Azure DevOps SSH clone URL uses the ssh.dev.azure.com host.
89 expect(
90 detectSourceControlProviderFromRemoteUrl("git@ssh.dev.azure.com:v3/org/project/repo")?.kind,
91 ).toBe("azure-devops");
92 expect(
93 detectSourceControlProviderFromRemoteUrl("ssh://git@ssh.dev.azure.com:22/v3/org/project/repo")
94 ?.kind,
95 ).toBe("azure-devops");
96 // Legacy visualstudio.com SSH host stays classified too.
97 expect(
98 detectSourceControlProviderFromRemoteUrl("git@vs-ssh.visualstudio.com:v3/org/project/repo")
99 ?.kind,
100 ).toBe("azure-devops");
101 });
102
103 it("preserves ports while classifying by hostname", () => {
104 expect(
105 detectSourceControlProviderFromRemoteUrl("https://gitlab.com:8443/group/repo.git"),
106 ).toEqual({
107 kind: "gitlab",
108 name: "GitLab",
109 baseUrl: "https://gitlab.com:8443",
110 });
111 expect(
112 detectSourceControlProviderFromRemoteUrl(
113 "https://self-hosted.example.test:8443/group/repo.git",
114 ),
115 ).toEqual({
116 kind: "unknown",
117 name: "self-hosted.example.test:8443",
118 baseUrl: "https://self-hosted.example.test:8443",
119 });
120 });
121
122 it("does not reuse SSH ports for HTTPS provider URLs", () => {
123 expect(
124 detectSourceControlProviderFromRemoteUrl("ssh://git@gitlab.example.test:24/group/repo.git"),
125 ).toEqual({
126 kind: "gitlab",
127 name: "GitLab Self-Hosted",
128 baseUrl: "https://gitlab.example.test",
129 });
130 expect(
131 detectSourceControlProviderFromRemoteUrl("ssh://git@code.example.test:24/team/project.git"),
132 ).toEqual({
133 kind: "unknown",
134 name: "code.example.test",
135 baseUrl: "https://code.example.test",
136 });
137 });
138
139 it("matches self-hosted providers by complete DNS labels", () => {
140 expect(
141 detectSourceControlProviderFromRemoteUrl("https://github.example.com/owner/repo.git")?.kind,
142 ).toBe("github");
143 expect(
144 detectSourceControlProviderFromRemoteUrl("https://gitlab.example.com/group/repo.git")?.kind,
145 ).toBe("gitlab");
146 expect(
147 detectSourceControlProviderFromRemoteUrl("https://bitbucket.example.com/workspace/repo.git")
148 ?.kind,
149 ).toBe("bitbucket");
150 });
151
152 it("does not match provider names embedded in unrelated DNS labels", () => {
153 expect(
154 detectSourceControlProviderFromRemoteUrl("https://notgithub.example.com/owner/repo.git")
155 ?.kind,
156 ).toBe("unknown");
157 expect(
158 detectSourceControlProviderFromRemoteUrl("https://notgitlab.example.com/group/repo.git")
159 ?.kind,
160 ).toBe("unknown");
161 expect(
162 detectSourceControlProviderFromRemoteUrl(
163 "https://notbitbucket.example.com/workspace/repo.git",
164 )?.kind,
165 ).toBe("unknown");
166 });
167
168 it("detects SSH remotes with non-git SSH users (e.g. gitlab@, deploy@)", () => {
169 expect(
170 detectSourceControlProviderFromRemoteUrl("gitlab@gitlab.example.com:group/project.git")?.kind,
171 ).toBe("gitlab");
172 expect(
173 detectSourceControlProviderFromRemoteUrl("gitlab@gitlab.example.com:group/project.git")
174 ?.baseUrl,
175 ).toBe("https://gitlab.example.com");
176 expect(detectSourceControlProviderFromRemoteUrl("deploy@github.com:owner/repo.git")?.kind).toBe(
177 "github",
178 );
179 expect(
180 detectSourceControlProviderFromRemoteUrl("git@bitbucket.org:workspace/repo.git")?.kind,
181 ).toBe("bitbucket");
182 });
183});
184
185describe("isSshRemoteUrl", () => {
186 it("recognises SCP-like SSH URLs with any SSH user prefix", () => {
187 expect(isSshRemoteUrl("git@github.com:owner/repo.git")).toBe(true);
188 expect(isSshRemoteUrl("gitlab@gitlab.example.com:group/project.git")).toBe(true);
189 expect(isSshRemoteUrl("deploy@bitbucket.org:workspace/repo.git")).toBe(true);
190 });
191
192 it("recognises ssh:// URLs with any case", () => {
193 expect(isSshRemoteUrl("ssh://git@gitlab.example.com/group/project.git")).toBe(true);
194 expect(isSshRemoteUrl("ssh://git@gitlab.example.com:22/group/project.git")).toBe(true);
195 expect(isSshRemoteUrl("SSH://git@gitlab.example.com/group/project.git")).toBe(true);
196 expect(isSshRemoteUrl("SsH://git@gitlab.example.com/group/project.git")).toBe(true);
197 });
198
199 it("returns false for HTTPS, local paths, and SCP-like paths without a colon", () => {
200 expect(isSshRemoteUrl("https://gitlab.example.com/group/project.git")).toBe(false);
201 expect(isSshRemoteUrl("/home/user/repos/project")).toBe(false);
202 expect(isSshRemoteUrl("")).toBe(false);
203 expect(isSshRemoteUrl("deploy@github.com/project/repo")).toBe(false);
204 });
205});
206
207it("names an Azure DevOps repository by its own name, not its project path", () => {
208 // `az repos pr list --repository` takes a name and detects the organisation and project from
209 // the checkout; the recorded `org/project/_git/repo` path is refused, and the repository then
210 // reads as unavailable on the page.
211 const selector = sourceControlRepositorySelector({
212 provider: "azure-devops",
213 displayName: "contoso/payments/_git/checkout",
214 owner: "contoso",
215 name: "checkout",
216 });
217 expect(selector).toBe("checkout");
218});
219
220it("falls back to the path's last segment where an Azure identity has no name", () => {
221 const selector = sourceControlRepositorySelector({
222 provider: "azure-devops",
223 displayName: "contoso/payments/_git/checkout",
224 });
225 expect(selector).toBe("checkout");
226});
227
228it("keeps a GitLab identity's whole path, because a nested group is part of the name", () => {
229 const selector = sourceControlRepositorySelector({
230 provider: "gitlab",
231 displayName: "group/subgroup/service",
232 owner: "group",
233 name: "service",
234 });
235 expect(selector).toBe("group/subgroup/service");
236});
237
238it("puts owner and name back together for an identity recorded before displayName", () => {
239 const selector = sourceControlRepositorySelector({
240 provider: "github",
241 owner: "t3tools",
242 name: "t3code",
243 });
244 expect(selector).toBe("t3tools/t3code");
245});
246
247it("names nothing for a project with no remote to name it by", () => {
248 expect(sourceControlRepositorySelector(null)).toBeNull();
249 expect(sourceControlRepositorySelector({ provider: "github" })).toBeNull();
250});