authorgravatar for 269035359+t3-code[bot]@users.noreply.github.comt3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> 2026-08-21 04:55:14-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-21 04:55:14-04:00
log549201fcfd3baa6cf92f09e5d868c536c80d2d55
treee4fe7e2ac3ae1921540a172591f08b95c833e8e0
parent9f12eab383ab0fd348c9d35cd248cd939a2c5796
signature Signed by PGP key B5690EEEBB952194

fix(clients): default GitHub clones to HTTPS (#7760)

Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>

4 files changed, 68 insertions(+), 4 deletions(-)

apps/mobile/src/features/projects/AddProjectScreen.tsx+4-2
......@@ -10,6 +10,8 @@ import {
1010 getCloneDestinationBrowsePath,
1111 getCloneDestinationPath,
1212 getCloneDirectoryName,
13 getDefaultCloneUrl,
14 normalizePastedCloneUrl,
1315 resolveAddProjectPath,
1416 sortAddProjectProviderSources,
1517 type AddProjectRemoteSource,
......@@ -662,7 +664,7 @@ export function AddProjectRepositoryScreen(props: {
662664 setIsSubmitting(true);
663665 const provider = addProjectRemoteSourceProvider(source);
664666 if (!provider) {
665 const remoteUrl = repositoryInput.trim();
667 const remoteUrl = normalizePastedCloneUrl(repositoryInput);
666668 navigation.dispatch(
667669 StackActions.push("AddProjectDestination", {
668670 environmentId: environment.environmentId,
......@@ -691,7 +693,7 @@ export function AddProjectRepositoryScreen(props: {
691693 StackActions.push("AddProjectDestination", {
692694 environmentId: environment.environmentId,
693695 source,
694 remoteUrl: repository.sshUrl,
696 remoteUrl: getDefaultCloneUrl(repository),
695697 repositoryTitle: repository.nameWithOwner,
696698 repositoryName: getCloneDirectoryName(repository.nameWithOwner),
697699 }),
apps/web/src/components/CommandPalette.tsx+4-2
......@@ -6,6 +6,8 @@ import {
66 getCloneDestinationBrowsePath,
77 getCloneDestinationPath,
88 getCloneDirectoryName,
9 getDefaultCloneUrl,
10 normalizePastedCloneUrl,
911} from "@t3tools/client-runtime/operations/projects";
1012import { connectionStatusText } from "@t3tools/client-runtime/connection";
1113import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";
......@@ -1874,7 +1876,7 @@ function OpenCommandPaletteDialog(props: {
18741876 source: addProjectCloneFlow.source,
18751877 repositoryInput: rawRepository,
18761878 repository: null,
1877 remoteUrl: rawRepository,
1879 remoteUrl: normalizePastedCloneUrl(rawRepository),
18781880 });
18791881 setHighlightedItemValue(null);
18801882 setQuery(destinationPath);
......@@ -1914,7 +1916,7 @@ function OpenCommandPaletteDialog(props: {
19141916 source: addProjectCloneFlow.source,
19151917 repositoryInput: rawRepository,
19161918 repository,
1917 remoteUrl: repository.sshUrl,
1919 remoteUrl: getDefaultCloneUrl(repository),
19181920 });
19191921 setHighlightedItemValue(null);
19201922 setQuery(destinationPath);
packages/client-runtime/src/operations/projects.test.ts+42
......@@ -16,6 +16,8 @@ import {
1616 getCloneDestinationBrowsePath,
1717 getCloneDestinationPath,
1818 getCloneDirectoryName,
19 getDefaultCloneUrl,
20 normalizePastedCloneUrl,
1921 resolveAddProjectPath,
2022 sortAddProjectProviderSources,
2123} from "./projects.ts";
......@@ -46,6 +48,46 @@ describe("add project shared logic", () => {
4648 expect(getCloneDirectoryName(null)).toBe("");
4749 });
4850
51 it("routes owner/repository shorthand to GitHub over HTTPS", () => {
52 expect(normalizePastedCloneUrl("imputnet/helium")).toBe(
53 "https://github.com/imputnet/helium.git",
54 );
55 expect(normalizePastedCloneUrl(" pingdotgg/t3code ")).toBe(
56 "https://github.com/pingdotgg/t3code.git",
57 );
58 });
59
60 it("keeps explicit clone URLs and local paths unchanged", () => {
61 expect(normalizePastedCloneUrl("https://gitlab.com/group/project.git")).toBe(
62 "https://gitlab.com/group/project.git",
63 );
64 expect(normalizePastedCloneUrl("git@github.com:owner/repo.git")).toBe(
65 "git@github.com:owner/repo.git",
66 );
67 expect(normalizePastedCloneUrl("group/subgroup/project")).toBe("group/subgroup/project");
68 expect(normalizePastedCloneUrl("/srv/git/repo.git")).toBe("/srv/git/repo.git");
69 });
70
71 it("uses HTTPS for repositories selected through a provider", () => {
72 expect(
73 getDefaultCloneUrl({
74 provider: "github",
75 url: "https://github.com/imputnet/helium",
76 sshUrl: "git@github.com:imputnet/helium.git",
77 }),
78 ).toBe("https://github.com/imputnet/helium");
79 });
80
81 it("preserves existing clone transport behavior for other providers", () => {
82 expect(
83 getDefaultCloneUrl({
84 provider: "gitlab",
85 url: "https://gitlab.com/group/project.git",
86 sshUrl: "git@gitlab.com:group/project.git",
87 }),
88 ).toBe("git@gitlab.com:group/project.git");
89 });
90
4991 it("derives the clone folder name from any pasted clone URL", () => {
5092 expect(getCloneDirectoryName("https://github.com/owner/repo.git")).toBe("repo");
5193 expect(getCloneDirectoryName("https://github.com/owner/repo")).toBe("repo");
packages/client-runtime/src/operations/projects.ts+18
......@@ -106,6 +106,24 @@ export function addProjectRemoteSourceProvider(
106106 return source === "url" ? null : source;
107107}
108108
109const GITHUB_REPOSITORY_SHORTHAND =
110 /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]+(?:\.git)?$/;
111
112/** Treat the common owner/repository shorthand as a public GitHub HTTPS URL. */
113export function normalizePastedCloneUrl(input: string): string {
114 const trimmed = input.trim();
115 if (!GITHUB_REPOSITORY_SHORTHAND.test(trimmed)) return trimmed;
116 const repository = trimmed.endsWith(".git") ? trimmed : `${trimmed}.git`;
117 return `https://github.com/${repository}`;
118}
119
120/** GitHub defaults to HTTPS; other providers retain their existing SSH default. */
121export function getDefaultCloneUrl(
122 repository: Pick<SourceControlRepositoryInfo, "provider" | "url" | "sshUrl">,
123): string {
124 return repository.provider === "github" ? repository.url : repository.sshUrl;
125}
126
109127export function sortAddProjectProviderSources(
110128 readinessBySource: AddProjectRemoteSourceReadiness,
111129): ReadonlyArray<AddProjectRemoteProviderKind> {