Skip to content
Merged
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 @@ -23,6 +23,7 @@ import {
getNewProjectPublishTarget,
getNewProjectPublishTargets,
getNewProjectRepository,
githubIncompleteOwnerRepositoryMessage,
defaultHostCloneUrl,
normalizePastedCloneUrl,
parseAddProjectRemoteSource,
Expand Down Expand Up @@ -823,6 +824,11 @@ export function AddProjectRepositoryScreen(props: {

const lookupRepository = 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 @@ -16,6 +16,7 @@ import {
getNewProjectPublishTarget,
getNewProjectPublishTargets,
getNewProjectRepository,
githubIncompleteOwnerRepositoryMessage,
normalizePastedCloneUrl,
addProjectRemoteSourceLabel,
addProjectRemoteSourcePathHint,
Expand Down Expand Up @@ -1939,6 +1940,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 = addProjectRemoteSourceProvider(addProjectCloneFlow.source);
if (!provider) {
const destinationPath = getCloneDestinationPath(
Expand Down
31 changes: 31 additions & 0 deletions packages/client-runtime/src/operations/projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,42 @@ import {
findExistingAddProject,
getCloneDirectoryName,
getDefaultCloneUrl,
githubIncompleteOwnerRepositoryMessage,
normalizePastedCloneUrl,
} 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", () => {
const github = SourceControlProviderKind.make("github");
expect(githubIncompleteOwnerRepositoryMessage(github, "pingdotgg/")).toBe(
"Enter the full owner/repository name.",
);
expect(githubIncompleteOwnerRepositoryMessage(github, " pingdotgg// ")).toBe(
"Enter the full owner/repository name.",
);
expect(githubIncompleteOwnerRepositoryMessage(github, "mona-cat_octo/")).toBe(
"Enter the full owner/repository name.",
);

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

it("routes owner/repository shorthand to GitHub over HTTPS", () => {
expect(normalizePastedCloneUrl("imputnet/helium")).toBe(
"https://github.com/imputnet/helium.git",
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 @@ -106,6 +106,25 @@ export function defaultHostCloneUrl(repository: string): string | null {
: `git@${definition.publicHost}:${path}`;
}

/** 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;
}

export function normalizePastedCloneUrl(input: string): string {
const trimmed = input.trim();
return REPOSITORY_SHORTHAND.test(trimmed) ? (defaultHostCloneUrl(trimmed) ?? trimmed) : trimmed;
Expand Down
Loading