Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getNewProjectGitHubRepository,
getNewProjectGitHubTarget,
getNewProjectPathPreview,
githubIncompleteOwnerRepositoryMessage,
normalizePastedCloneUrl,
resolveAddProjectPath,
sortAddProjectProviderSources,
Expand Down Expand Up @@ -755,6 +756,11 @@ export function AddProjectRepositoryScreen(props: {

const lookupRepository = useCallback(async () => {
if (!environment || repositoryInput.trim().length === 0 || isSubmitting) return;
const incompleteOwnerMessage = githubIncompleteOwnerRepositoryMessage(source, repositoryInput);
if (incompleteOwnerMessage) {
setError(incompleteOwnerMessage);
return;
}
setError(null);
setIsSubmitting(true);
const provider = addProjectRemoteSourceProvider(source);
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getNewProjectGitHubRepository,
getNewProjectGitHubTarget,
getNewProjectPathPreview,
githubIncompleteOwnerRepositoryMessage,
normalizePastedCloneUrl,
} from "@t3tools/client-runtime/operations/projects";
import { connectionStatusText } from "@t3tools/client-runtime/connection";
Expand Down Expand Up @@ -2561,6 +2562,21 @@ function OpenCommandPaletteDialog(props: {
return;
}

const incompleteOwnerMessage = githubIncompleteOwnerRepositoryMessage(
addProjectCloneFlow.source,
rawRepository,
);
if (incompleteOwnerMessage) {
toastManager.add(
stackedThreadToast({
type: "error",
title: "Repository name incomplete",
description: incompleteOwnerMessage,
}),
);
return;
}

const provider = remoteProjectSourceProvider(addProjectCloneFlow.source);
if (!provider) {
const destinationPath = getCloneDestinationPath(
Expand Down
22 changes: 22 additions & 0 deletions packages/client-runtime/src/operations/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,35 @@ import {
getCloneDestinationPath,
getCloneDirectoryName,
getDefaultCloneUrl,
githubIncompleteOwnerRepositoryMessage,
normalizePastedCloneUrl,
resolveAddProjectPath,
sortAddProjectProviderSources,
} from "./projects.ts";
import type { EnvironmentProject } from "../state/models.ts";

describe("add project shared logic", () => {
it("asks for a repository name when GitHub input is only an owner path", () => {
expect(githubIncompleteOwnerRepositoryMessage("github", "pingdotgg/")).toBe(
"Enter the full owner/repository name.",
);
expect(githubIncompleteOwnerRepositoryMessage("github", " pingdotgg// ")).toBe(
"Enter the full owner/repository name.",
);

for (const input of [
"pingdotgg/t3code",
"pingdotgg",
"https://github.com/pingdotgg/t3code.git",
"git@github.com:pingdotgg/t3code.git",
]) {
expect(githubIncompleteOwnerRepositoryMessage("github", input)).toBeNull();
}
expect(githubIncompleteOwnerRepositoryMessage("gitlab", "group/")).toBeNull();
expect(githubIncompleteOwnerRepositoryMessage("url", "owner/")).toBeNull();
expect(githubIncompleteOwnerRepositoryMessage("forgejo", "owner/")).toBeNull();
});

it("only allows project creation in connected environments", () => {
expect(canCreateProjectInEnvironment("connected")).toBe(true);
expect(canCreateProjectInEnvironment("available")).toBe(false);
Expand Down
19 changes: 19 additions & 0 deletions packages/client-runtime/src/operations/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ export function addProjectRemoteSourceProvider(
const GITHUB_REPOSITORY_SHORTHAND =
/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]+(?:\.git)?$/;

/** Owner present, repository name missing: `owner/` or `owner//`. */
const INCOMPLETE_GITHUB_OWNER_PATH = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/+$/;

const INCOMPLETE_GITHUB_OWNER_REPOSITORY_MESSAGE = "Enter the full owner/repository name.";

/**
* Message to show when a GitHub add-project field is only `owner/`.
* Lookup would send that to GitHub and come back as a generic failure.
* Returns null for complete names, bare repo names, URLs, and other sources.
*/
export function githubIncompleteOwnerRepositoryMessage(
source: AddProjectRemoteSource,
input: string,
): string | null {
if (source !== "github") return null;
if (!INCOMPLETE_GITHUB_OWNER_PATH.test(input.trim())) return null;
return INCOMPLETE_GITHUB_OWNER_REPOSITORY_MESSAGE;
}

/** Treat the common owner/repository shorthand as a public GitHub HTTPS URL. */
export function normalizePastedCloneUrl(input: string): string {
const trimmed = input.trim();
Expand Down
Loading