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 {...@@ -10,6 +10,8 @@ import {
10 getCloneDestinationBrowsePath,10 getCloneDestinationBrowsePath,
11 getCloneDestinationPath,11 getCloneDestinationPath,
12 getCloneDirectoryName,12 getCloneDirectoryName,
13 getDefaultCloneUrl,
14 normalizePastedCloneUrl,
13 resolveAddProjectPath,15 resolveAddProjectPath,
14 sortAddProjectProviderSources,16 sortAddProjectProviderSources,
15 type AddProjectRemoteSource,17 type AddProjectRemoteSource,
...@@ -662,7 +664,7 @@ export function AddProjectRepositoryScreen(props: {...@@ -662,7 +664,7 @@ export function AddProjectRepositoryScreen(props: {
662 setIsSubmitting(true);664 setIsSubmitting(true);
663 const provider = addProjectRemoteSourceProvider(source);665 const provider = addProjectRemoteSourceProvider(source);
664 if (!provider) {666 if (!provider) {
665 const remoteUrl = repositoryInput.trim();667 const remoteUrl = normalizePastedCloneUrl(repositoryInput);
666 navigation.dispatch(668 navigation.dispatch(
667 StackActions.push("AddProjectDestination", {669 StackActions.push("AddProjectDestination", {
668 environmentId: environment.environmentId,670 environmentId: environment.environmentId,
...@@ -691,7 +693,7 @@ export function AddProjectRepositoryScreen(props: {...@@ -691,7 +693,7 @@ export function AddProjectRepositoryScreen(props: {
691 StackActions.push("AddProjectDestination", {693 StackActions.push("AddProjectDestination", {
692 environmentId: environment.environmentId,694 environmentId: environment.environmentId,
693 source,695 source,
694 remoteUrl: repository.sshUrl,696 remoteUrl: getDefaultCloneUrl(repository),
695 repositoryTitle: repository.nameWithOwner,697 repositoryTitle: repository.nameWithOwner,
696 repositoryName: getCloneDirectoryName(repository.nameWithOwner),698 repositoryName: getCloneDirectoryName(repository.nameWithOwner),
697 }),699 }),
apps/web/src/components/CommandPalette.tsx+4-2
...@@ -6,6 +6,8 @@ import {...@@ -6,6 +6,8 @@ import {
6 getCloneDestinationBrowsePath,6 getCloneDestinationBrowsePath,
7 getCloneDestinationPath,7 getCloneDestinationPath,
8 getCloneDirectoryName,8 getCloneDirectoryName,
9 getDefaultCloneUrl,
10 normalizePastedCloneUrl,
9} from "@t3tools/client-runtime/operations/projects";11} from "@t3tools/client-runtime/operations/projects";
10import { connectionStatusText } from "@t3tools/client-runtime/connection";12import { connectionStatusText } from "@t3tools/client-runtime/connection";
11import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";13import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";
...@@ -1874,7 +1876,7 @@ function OpenCommandPaletteDialog(props: {...@@ -1874,7 +1876,7 @@ function OpenCommandPaletteDialog(props: {
1874 source: addProjectCloneFlow.source,1876 source: addProjectCloneFlow.source,
1875 repositoryInput: rawRepository,1877 repositoryInput: rawRepository,
1876 repository: null,1878 repository: null,
1877 remoteUrl: rawRepository,1879 remoteUrl: normalizePastedCloneUrl(rawRepository),
1878 });1880 });
1879 setHighlightedItemValue(null);1881 setHighlightedItemValue(null);
1880 setQuery(destinationPath);1882 setQuery(destinationPath);
...@@ -1914,7 +1916,7 @@ function OpenCommandPaletteDialog(props: {...@@ -1914,7 +1916,7 @@ function OpenCommandPaletteDialog(props: {
1914 source: addProjectCloneFlow.source,1916 source: addProjectCloneFlow.source,
1915 repositoryInput: rawRepository,1917 repositoryInput: rawRepository,
1916 repository,1918 repository,
1917 remoteUrl: repository.sshUrl,1919 remoteUrl: getDefaultCloneUrl(repository),
1918 });1920 });
1919 setHighlightedItemValue(null);1921 setHighlightedItemValue(null);
1920 setQuery(destinationPath);1922 setQuery(destinationPath);
packages/client-runtime/src/operations/projects.test.ts+42
...@@ -16,6 +16,8 @@ import {...@@ -16,6 +16,8 @@ import {
16 getCloneDestinationBrowsePath,16 getCloneDestinationBrowsePath,
17 getCloneDestinationPath,17 getCloneDestinationPath,
18 getCloneDirectoryName,18 getCloneDirectoryName,
19 getDefaultCloneUrl,
20 normalizePastedCloneUrl,
19 resolveAddProjectPath,21 resolveAddProjectPath,
20 sortAddProjectProviderSources,22 sortAddProjectProviderSources,
21} from "./projects.ts";23} from "./projects.ts";
...@@ -46,6 +48,46 @@ describe("add project shared logic", () => {...@@ -46,6 +48,46 @@ describe("add project shared logic", () => {
46 expect(getCloneDirectoryName(null)).toBe("");48 expect(getCloneDirectoryName(null)).toBe("");
47 });49 });
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
49 it("derives the clone folder name from any pasted clone URL", () => {91 it("derives the clone folder name from any pasted clone URL", () => {
50 expect(getCloneDirectoryName("https://github.com/owner/repo.git")).toBe("repo");92 expect(getCloneDirectoryName("https://github.com/owner/repo.git")).toBe("repo");
51 expect(getCloneDirectoryName("https://github.com/owner/repo")).toBe("repo");93 expect(getCloneDirectoryName("https://github.com/owner/repo")).toBe("repo");
packages/client-runtime/src/operations/projects.ts+18
...@@ -106,6 +106,24 @@ export function addProjectRemoteSourceProvider(...@@ -106,6 +106,24 @@ export function addProjectRemoteSourceProvider(
106 return source === "url" ? null : source;106 return source === "url" ? null : source;
107}107}
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
109export function sortAddProjectProviderSources(127export function sortAddProjectProviderSources(
110 readinessBySource: AddProjectRemoteSourceReadiness,128 readinessBySource: AddProjectRemoteSourceReadiness,
111): ReadonlyArray<AddProjectRemoteProviderKind> {129): ReadonlyArray<AddProjectRemoteProviderKind> {