| author | |
| committer | |
| log | 7ce4194707e2a883ccd6a44fd2e07eba08f5369a |
| tree | 5d3a3a5e817182faddd694a7246987d93e328bbc |
| parent | dc0ff8f13eaa9753fdf0de60203c9e819f1cee27 |
| signature | Signed by PGP key B5690EEEBB952194 |
6 files changed, 72 insertions(+), 16 deletions(-)
apps/server/src/git/GitManager.ts+2-2| ... | ... | @@ -42,6 +42,7 @@ import { |
| 42 | 42 | } from "@t3tools/shared/git"; |
| 43 | 43 | import { |
| 44 | 44 | getChangeRequestTerminologyForKind, |
| 45 | isSshRemoteUrl, | |
| 45 | 46 | type ChangeRequestTerminology, |
| 46 | 47 | } from "@t3tools/shared/sourceControl"; |
| 47 | 48 | |
| ... | ... | @@ -574,8 +575,7 @@ function toResolvedPullRequest(pr: { |
| 574 | 575 | |
| 575 | 576 | function shouldPreferSshRemote(url: string | null): boolean { |
| 576 | 577 | if (!url) return false; |
| 577 | const trimmed = url.trim(); | |
| 578 | return trimmed.startsWith("git@") || trimmed.startsWith("ssh://"); | |
| 578 | return isSshRemoteUrl(url); | |
| 579 | 579 | } |
| 580 | 580 | |
| 581 | 581 | function toPullRequestHeadRemoteInfo(pr: { |
apps/server/src/sourceControl/BitbucketApi.ts+9-6| ... | ... | @@ -14,7 +14,10 @@ import { |
| 14 | 14 | } from "@t3tools/contracts"; |
| 15 | 15 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; |
| 16 | 16 | import { sanitizeBranchFragment } from "@t3tools/shared/git"; |
| 17 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; | |
| 17 | import { | |
| 18 | detectSourceControlProviderFromRemoteUrl, | |
| 19 | isSshRemoteUrl, | |
| 20 | } from "@t3tools/shared/sourceControl"; | |
| 18 | 21 | |
| 19 | 22 | import { |
| 20 | 23 | BitbucketPullRequestListSchema, |
| ... | ... | @@ -448,9 +451,9 @@ function requireRepositoryLocator( |
| 448 | 451 | |
| 449 | 452 | function parseBitbucketRemoteUrl(remoteUrl: string): BitbucketRepositoryLocator | null { |
| 450 | 453 | const trimmed = remoteUrl.trim(); |
| 451 | if (trimmed.startsWith("git@")) { | |
| 452 | const pathStart = trimmed.indexOf(":"); | |
| 453 | return pathStart < 0 ? null : parseBitbucketRepositorySlug(trimmed.slice(pathStart + 1)); | |
| 454 | const scpMatch = /^[a-zA-Z0-9._-]+@[^:/]+:(.+)$/.exec(trimmed); | |
| 455 | if (scpMatch?.[1]) { | |
| 456 | return parseBitbucketRepositorySlug(scpMatch[1]); | |
| 454 | 457 | } |
| 455 | 458 | |
| 456 | 459 | try { |
| ... | ... | @@ -494,8 +497,8 @@ function defaultChangeRequestTargetBranch(input: { |
| 494 | 497 | } |
| 495 | 498 | |
| 496 | 499 | function shouldPreferSshRemote(originRemoteUrl: string | null): boolean { |
| 497 | const trimmed = originRemoteUrl?.trim() ?? ""; | |
| 498 | return trimmed.startsWith("git@") || trimmed.startsWith("ssh://"); | |
| 500 | if (!originRemoteUrl) return false; | |
| 501 | return isSshRemoteUrl(originRemoteUrl); | |
| 499 | 502 | } |
| 500 | 503 | |
| 501 | 504 | function selectCloneUrl(input: { |
packages/shared/src/git.test.ts+9| ... | ... | @@ -40,6 +40,15 @@ describe("normalizeGitRemoteUrl", () => { |
| 40 | 40 | "gitlab.company.com/team/project", |
| 41 | 41 | ); |
| 42 | 42 | }); |
| 43 | ||
| 44 | it("normalizes SCP-like remotes with non-git SSH users", () => { | |
| 45 | expect(normalizeGitRemoteUrl("gitlab@gitlab.example.com:group/project.git")).toBe( | |
| 46 | "gitlab.example.com/group/project", | |
| 47 | ); | |
| 48 | expect(normalizeGitRemoteUrl("deploy@bitbucket.org:workspace/repo.git")).toBe( | |
| 49 | "bitbucket.org/workspace/repo", | |
| 50 | ); | |
| 51 | }); | |
| 43 | 52 | }); |
| 44 | 53 | |
| 45 | 54 | describe("parseGitHubRepositoryNameWithOwnerFromRemoteUrl", () => { |
packages/shared/src/git.ts+3-1| ... | ... | @@ -133,7 +133,9 @@ export function normalizeGitRemoteUrl(value: string): string { |
| 133 | 133 | } |
| 134 | 134 | } |
| 135 | 135 | |
| 136 | const scpStyleHostAndPath = /^git@([^:/\s]+)[:/]([^/\s]+(?:\/[^/\s]+)+)$/i.exec(normalized); | |
| 136 | const scpStyleHostAndPath = /^[a-zA-Z0-9._-]+@([^:/\s]+):([^/\s]+(?:\/[^/\s]+)+)$/i.exec( | |
| 137 | normalized, | |
| 138 | ); | |
| 137 | 139 | if (scpStyleHostAndPath?.[1] && scpStyleHostAndPath[2]) { |
| 138 | 140 | return `${scpStyleHostAndPath[1]}/${scpStyleHostAndPath[2]}`; |
| 139 | 141 | } |
packages/shared/src/sourceControl.test.ts+39| ... | ... | @@ -3,6 +3,7 @@ import { describe, expect, it } from "vite-plus/test"; |
| 3 | 3 | import { |
| 4 | 4 | detectSourceControlProviderFromRemoteUrl, |
| 5 | 5 | getChangeRequestTerminologyForKind, |
| 6 | isSshRemoteUrl, | |
| 6 | 7 | resolveChangeRequestPresentation, |
| 7 | 8 | } from "./sourceControl.ts"; |
| 8 | 9 | |
| ... | ... | @@ -120,4 +121,42 @@ describe("detectSourceControlProviderFromRemoteUrl", () => { |
| 120 | 121 | )?.kind, |
| 121 | 122 | ).toBe("unknown"); |
| 122 | 123 | }); |
| 124 | ||
| 125 | it("detects SSH remotes with non-git SSH users (e.g. gitlab@, deploy@)", () => { | |
| 126 | expect( | |
| 127 | detectSourceControlProviderFromRemoteUrl("gitlab@gitlab.example.com:group/project.git")?.kind, | |
| 128 | ).toBe("gitlab"); | |
| 129 | expect( | |
| 130 | detectSourceControlProviderFromRemoteUrl("gitlab@gitlab.example.com:group/project.git") | |
| 131 | ?.baseUrl, | |
| 132 | ).toBe("https://gitlab.example.com"); | |
| 133 | expect(detectSourceControlProviderFromRemoteUrl("deploy@github.com:owner/repo.git")?.kind).toBe( | |
| 134 | "github", | |
| 135 | ); | |
| 136 | expect( | |
| 137 | detectSourceControlProviderFromRemoteUrl("git@bitbucket.org:workspace/repo.git")?.kind, | |
| 138 | ).toBe("bitbucket"); | |
| 139 | }); | |
| 140 | }); | |
| 141 | ||
| 142 | describe("isSshRemoteUrl", () => { | |
| 143 | it("recognises SCP-like SSH URLs with any SSH user prefix", () => { | |
| 144 | expect(isSshRemoteUrl("git@github.com:owner/repo.git")).toBe(true); | |
| 145 | expect(isSshRemoteUrl("gitlab@gitlab.example.com:group/project.git")).toBe(true); | |
| 146 | expect(isSshRemoteUrl("deploy@bitbucket.org:workspace/repo.git")).toBe(true); | |
| 147 | }); | |
| 148 | ||
| 149 | it("recognises ssh:// URLs with any case", () => { | |
| 150 | expect(isSshRemoteUrl("ssh://git@gitlab.example.com/group/project.git")).toBe(true); | |
| 151 | expect(isSshRemoteUrl("ssh://git@gitlab.example.com:22/group/project.git")).toBe(true); | |
| 152 | expect(isSshRemoteUrl("SSH://git@gitlab.example.com/group/project.git")).toBe(true); | |
| 153 | expect(isSshRemoteUrl("SsH://git@gitlab.example.com/group/project.git")).toBe(true); | |
| 154 | }); | |
| 155 | ||
| 156 | it("returns false for HTTPS, local paths, and SCP-like paths without a colon", () => { | |
| 157 | expect(isSshRemoteUrl("https://gitlab.example.com/group/project.git")).toBe(false); | |
| 158 | expect(isSshRemoteUrl("/home/user/repos/project")).toBe(false); | |
| 159 | expect(isSshRemoteUrl("")).toBe(false); | |
| 160 | expect(isSshRemoteUrl("deploy@github.com/project/repo")).toBe(false); | |
| 161 | }); | |
| 123 | 162 | }); |
packages/shared/src/sourceControl.ts+10-7| ... | ... | @@ -133,19 +133,22 @@ export function getChangeRequestTerminologyForKind( |
| 133 | 133 | }; |
| 134 | 134 | } |
| 135 | 135 | |
| 136 | const SCP_SSH_REMOTE_PATTERN = /^[a-zA-Z0-9._-]+@([^:/]+):/; | |
| 137 | ||
| 138 | export function isSshRemoteUrl(remoteUrl: string): boolean { | |
| 139 | const trimmed = remoteUrl.trim(); | |
| 140 | return SCP_SSH_REMOTE_PATTERN.test(trimmed) || trimmed.toLowerCase().startsWith("ssh://"); | |
| 141 | } | |
| 142 | ||
| 136 | 143 | function parseRemoteHost(remoteUrl: string): string | null { |
| 137 | 144 | const trimmed = remoteUrl.trim(); |
| 138 | 145 | if (trimmed.length === 0) { |
| 139 | 146 | return null; |
| 140 | 147 | } |
| 141 | 148 | |
| 142 | if (trimmed.startsWith("git@")) { | |
| 143 | const hostWithPath = trimmed.slice("git@".length); | |
| 144 | const separatorIndex = hostWithPath.search(/[:/]/); | |
| 145 | if (separatorIndex <= 0) { | |
| 146 | return null; | |
| 147 | } | |
| 148 | return hostWithPath.slice(0, separatorIndex).toLowerCase(); | |
| 149 | const scpMatch = SCP_SSH_REMOTE_PATTERN.exec(trimmed); | |
| 150 | if (scpMatch?.[1]) { | |
| 151 | return scpMatch[1].toLowerCase(); | |
| 149 | 152 | } |
| 150 | 153 | |
| 151 | 154 | try { |