mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-11 04:22:13 +00:00
[cli] MAJOR: Connect a Git provider repository in vc link (#8290)
#8100 added a new `vc git` command, which allows users to connect a Git provider repository, enabling them to set up a full Git workflow for their Vercel projects without having to leave the CLI. This PR takes this functionality a step further by including it as part of the `vc link` flow. This way, users can set up a Vercel project and add a Git provider repository all in one step. This PR is blocked by a PR to `front` which adds an option to the Git Settings page for a Project to re-enable the prompt if the user opted out (in review). ### 📋 Checklist <!-- Please keep your PR as a Draft until the checklist is complete --> #### Tests - [x] The code changed/added as part of this PR has been covered with tests - [x] All tests pass locally with `yarn test-unit` #### Code Review - [ ] This PR has a concise title and thorough description useful to a reviewer - [ ] Issue from task tracker has a link to this PR
This commit is contained in:
@@ -335,6 +335,7 @@ export interface ProjectSettings {
|
|||||||
directoryListing?: boolean;
|
directoryListing?: boolean;
|
||||||
gitForkProtection?: boolean;
|
gitForkProtection?: boolean;
|
||||||
commandForIgnoringBuildStep?: string | null;
|
commandForIgnoringBuildStep?: string | null;
|
||||||
|
skipGitConnectDuringLink?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BuilderV2 {
|
export interface BuilderV2 {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
connectGitProvider,
|
connectGitProvider,
|
||||||
disconnectGitProvider,
|
disconnectGitProvider,
|
||||||
formatProvider,
|
formatProvider,
|
||||||
ParsedRepoUrl,
|
RepoInfo,
|
||||||
parseRepoUrl,
|
parseRepoUrl,
|
||||||
printRemoteUrls,
|
printRemoteUrls,
|
||||||
} from '../../util/git/connect-git-provider';
|
} from '../../util/git/connect-git-provider';
|
||||||
@@ -35,7 +35,7 @@ interface ConnectArgParams {
|
|||||||
org: Org;
|
org: Org;
|
||||||
project: Project;
|
project: Project;
|
||||||
confirm: boolean;
|
confirm: boolean;
|
||||||
repoInfo: ParsedRepoUrl;
|
repoInfo: RepoInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectGitArgParams extends ConnectArgParams {
|
interface ConnectGitArgParams extends ConnectArgParams {
|
||||||
@@ -45,7 +45,7 @@ interface ConnectGitArgParams extends ConnectArgParams {
|
|||||||
interface PromptConnectArgParams {
|
interface PromptConnectArgParams {
|
||||||
client: Client;
|
client: Client;
|
||||||
yes: boolean;
|
yes: boolean;
|
||||||
repoInfo: ParsedRepoUrl;
|
repoInfo: RepoInfo;
|
||||||
remoteUrls: Dictionary<string>;
|
remoteUrls: Dictionary<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,8 +155,8 @@ export default async function connect(
|
|||||||
|
|
||||||
output.log(`Connecting Git remote: ${link(remoteUrl)}`);
|
output.log(`Connecting Git remote: ${link(remoteUrl)}`);
|
||||||
|
|
||||||
const parsedUrl = parseRepoUrl(remoteUrl);
|
const repoInfo = parseRepoUrl(remoteUrl);
|
||||||
if (!parsedUrl) {
|
if (!repoInfo) {
|
||||||
output.error(
|
output.error(
|
||||||
`Failed to parse Git repo data from the following remote URL: ${link(
|
`Failed to parse Git repo data from the following remote URL: ${link(
|
||||||
remoteUrl
|
remoteUrl
|
||||||
@@ -164,7 +164,7 @@ export default async function connect(
|
|||||||
);
|
);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
const { provider, org: gitOrg, repo } = parsedUrl;
|
const { provider, org: gitOrg, repo } = repoInfo;
|
||||||
const repoPath = `${gitOrg}/${repo}`;
|
const repoPath = `${gitOrg}/${repo}`;
|
||||||
|
|
||||||
const checkAndConnect = await checkExistsAndConnect({
|
const checkAndConnect = await checkExistsAndConnect({
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { Org } from '../../types';
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import link from '../output/link';
|
import link from '../output/link';
|
||||||
import { isAPIError } from '../errors-ts';
|
import { isAPIError } from '../errors-ts';
|
||||||
import { Dictionary } from '@vercel/client';
|
|
||||||
import { Output } from '../output';
|
import { Output } from '../output';
|
||||||
|
import { Dictionary } from '@vercel/client';
|
||||||
|
|
||||||
export interface ParsedRepoUrl {
|
export interface RepoInfo {
|
||||||
url: string;
|
url: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
org: string;
|
org: string;
|
||||||
@@ -19,7 +19,7 @@ export async function disconnectGitProvider(
|
|||||||
org: Org,
|
org: Org,
|
||||||
projectId: string
|
projectId: string
|
||||||
) {
|
) {
|
||||||
const fetchUrl = `/v4/projects/${projectId}/link?${stringify({
|
const fetchUrl = `/v9/projects/${projectId}/link?${stringify({
|
||||||
teamId: org.type === 'team' ? org.id : undefined,
|
teamId: org.type === 'team' ? org.id : undefined,
|
||||||
})}`;
|
})}`;
|
||||||
return client.fetch(fetchUrl, {
|
return client.fetch(fetchUrl, {
|
||||||
@@ -37,7 +37,7 @@ export async function connectGitProvider(
|
|||||||
type: string,
|
type: string,
|
||||||
repo: string
|
repo: string
|
||||||
) {
|
) {
|
||||||
const fetchUrl = `/v4/projects/${projectId}/link?${stringify({
|
const fetchUrl = `/v9/projects/${projectId}/link?${stringify({
|
||||||
teamId: org.type === 'team' ? org.id : undefined,
|
teamId: org.type === 'team' ? org.id : undefined,
|
||||||
})}`;
|
})}`;
|
||||||
try {
|
try {
|
||||||
@@ -52,22 +52,21 @@ export async function connectGitProvider(
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (isAPIError(err)) {
|
const apiError = isAPIError(err);
|
||||||
if (
|
if (
|
||||||
err.action === 'Install GitHub App' ||
|
apiError &&
|
||||||
err.code === 'repo_not_found'
|
(err.action === 'Install GitHub App' || err.code === 'repo_not_found')
|
||||||
) {
|
) {
|
||||||
client.output.error(
|
client.output.error(
|
||||||
`Failed to link ${chalk.cyan(
|
`Failed to link ${chalk.cyan(
|
||||||
repo
|
repo
|
||||||
)}. Make sure there aren't any typos and that you have access to the repository if it's private.`
|
)}. Make sure there aren't any typos and that you have access to the repository if it's private.`
|
||||||
);
|
);
|
||||||
} else if (err.action === 'Add a Login Connection') {
|
} else if (apiError && err.action === 'Add a Login Connection') {
|
||||||
client.output.error(
|
client.output.error(
|
||||||
err.message.replace(repo, chalk.cyan(repo)) +
|
err.message.replace(repo, chalk.cyan(repo)) +
|
||||||
`\nVisit ${link(err.link)} for more information.`
|
`\nVisit ${link(err.link)} for more information.`
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
client.output.error(
|
client.output.error(
|
||||||
`Failed to connect the ${formatProvider(
|
`Failed to connect the ${formatProvider(
|
||||||
@@ -92,7 +91,7 @@ export function formatProvider(type: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseRepoUrl(originUrl: string): ParsedRepoUrl | null {
|
export function parseRepoUrl(originUrl: string): RepoInfo | null {
|
||||||
const isSSH = originUrl.startsWith('git@');
|
const isSSH = originUrl.startsWith('git@');
|
||||||
// Matches all characters between (// or @) and (.com or .org)
|
// Matches all characters between (// or @) and (.com or .org)
|
||||||
// eslint-disable-next-line prefer-named-capture-group
|
// eslint-disable-next-line prefer-named-capture-group
|
||||||
@@ -126,7 +125,6 @@ export function parseRepoUrl(originUrl: string): ParsedRepoUrl | null {
|
|||||||
repo,
|
repo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printRemoteUrls(
|
export function printRemoteUrls(
|
||||||
output: Output,
|
output: Output,
|
||||||
remoteUrls: Dictionary<string>
|
remoteUrls: Dictionary<string>
|
||||||
|
|||||||
107
packages/cli/src/util/link/add-git-connection.ts
Normal file
107
packages/cli/src/util/link/add-git-connection.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { Dictionary } from '@vercel/client';
|
||||||
|
import { parseRepoUrl } from '../git/connect-git-provider';
|
||||||
|
import Client from '../client';
|
||||||
|
import { Org, Project, ProjectSettings } from '../../types';
|
||||||
|
import { handleOptions } from './handle-options';
|
||||||
|
import {
|
||||||
|
promptGitConnectMultipleUrls,
|
||||||
|
promptGitConnectSingleUrl,
|
||||||
|
} from './git-connect-prompts';
|
||||||
|
|
||||||
|
function getProjectSettings(project: Project): ProjectSettings {
|
||||||
|
return {
|
||||||
|
createdAt: project.createdAt,
|
||||||
|
framework: project.framework,
|
||||||
|
devCommand: project.devCommand,
|
||||||
|
installCommand: project.installCommand,
|
||||||
|
buildCommand: project.buildCommand,
|
||||||
|
outputDirectory: project.outputDirectory,
|
||||||
|
rootDirectory: project.rootDirectory,
|
||||||
|
directoryListing: project.directoryListing,
|
||||||
|
nodeVersion: project.nodeVersion,
|
||||||
|
skipGitConnectDuringLink: project.skipGitConnectDuringLink,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addGitConnection(
|
||||||
|
client: Client,
|
||||||
|
org: Org,
|
||||||
|
project: Project,
|
||||||
|
remoteUrls: Dictionary<string>,
|
||||||
|
settings?: ProjectSettings
|
||||||
|
): Promise<number | void> {
|
||||||
|
if (!settings) {
|
||||||
|
settings = getProjectSettings(project);
|
||||||
|
}
|
||||||
|
if (Object.keys(remoteUrls).length === 1) {
|
||||||
|
return addSingleGitRemote(
|
||||||
|
client,
|
||||||
|
org,
|
||||||
|
project,
|
||||||
|
remoteUrls,
|
||||||
|
settings || project
|
||||||
|
);
|
||||||
|
} else if (Object.keys(remoteUrls).length > 1 && !project.link) {
|
||||||
|
return addMultipleGitRemotes(
|
||||||
|
client,
|
||||||
|
org,
|
||||||
|
project,
|
||||||
|
remoteUrls,
|
||||||
|
settings || project
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addSingleGitRemote(
|
||||||
|
client: Client,
|
||||||
|
org: Org,
|
||||||
|
project: Project,
|
||||||
|
remoteUrls: Dictionary<string>,
|
||||||
|
settings: ProjectSettings
|
||||||
|
) {
|
||||||
|
const [remoteName, remoteUrl] = Object.entries(remoteUrls)[0];
|
||||||
|
const repoInfo = parseRepoUrl(remoteUrl);
|
||||||
|
if (!repoInfo) {
|
||||||
|
client.output.debug(`Could not parse repo url ${repoInfo}.`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const { org: parsedOrg, repo, provider } = repoInfo;
|
||||||
|
const alreadyLinked =
|
||||||
|
project.link &&
|
||||||
|
project.link.org === parsedOrg &&
|
||||||
|
project.link.repo === repo &&
|
||||||
|
project.link.type === provider;
|
||||||
|
if (alreadyLinked) {
|
||||||
|
client.output.debug('Project already linked. Skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replace =
|
||||||
|
project.link &&
|
||||||
|
(project.link.org !== parsedOrg ||
|
||||||
|
project.link.repo !== repo ||
|
||||||
|
project.link.type !== provider);
|
||||||
|
const shouldConnect = await promptGitConnectSingleUrl(
|
||||||
|
client,
|
||||||
|
project,
|
||||||
|
remoteName,
|
||||||
|
remoteUrl,
|
||||||
|
replace
|
||||||
|
);
|
||||||
|
return handleOptions(shouldConnect, client, org, project, settings, repoInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addMultipleGitRemotes(
|
||||||
|
client: Client,
|
||||||
|
org: Org,
|
||||||
|
project: Project,
|
||||||
|
remoteUrls: Dictionary<string>,
|
||||||
|
settings: ProjectSettings
|
||||||
|
) {
|
||||||
|
client.output.log('Found multiple Git remote URLs in Git config.');
|
||||||
|
const remoteUrlOrOptions = await promptGitConnectMultipleUrls(
|
||||||
|
client,
|
||||||
|
remoteUrls
|
||||||
|
);
|
||||||
|
return handleOptions(remoteUrlOrOptions, client, org, project, settings);
|
||||||
|
}
|
||||||
86
packages/cli/src/util/link/git-connect-prompts.ts
Normal file
86
packages/cli/src/util/link/git-connect-prompts.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Dictionary } from '@vercel/client';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { Project } from '../../types';
|
||||||
|
import Client from '../client';
|
||||||
|
import { formatProvider } from '../git/connect-git-provider';
|
||||||
|
import list from '../input/list';
|
||||||
|
export async function promptGitConnectSingleUrl(
|
||||||
|
client: Client,
|
||||||
|
project: Project,
|
||||||
|
remoteName: string,
|
||||||
|
remoteUrl: string,
|
||||||
|
hasDiffConnectedProvider = false
|
||||||
|
) {
|
||||||
|
const { output } = client;
|
||||||
|
if (hasDiffConnectedProvider) {
|
||||||
|
const currentRepoPath = `${project.link!.org}/${project.link!.repo}`;
|
||||||
|
const currentProvider = project.link!.type;
|
||||||
|
output.print('\n');
|
||||||
|
output.log(
|
||||||
|
`Found Git remote URL ${chalk.cyan(
|
||||||
|
remoteUrl
|
||||||
|
)}, which is different from the connected ${formatProvider(
|
||||||
|
currentProvider
|
||||||
|
)} repository ${chalk.cyan(currentRepoPath)}.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
output.print('\n');
|
||||||
|
output.log(
|
||||||
|
`Found local Git remote "${remoteName}": ${chalk.cyan(remoteUrl)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await list(client, {
|
||||||
|
message: hasDiffConnectedProvider
|
||||||
|
? 'Do you want to replace it?'
|
||||||
|
: `Do you want to connect "${remoteName}" to your Vercel project?`,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
name: 'Yes',
|
||||||
|
value: 'yes',
|
||||||
|
short: 'yes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'No',
|
||||||
|
value: 'no',
|
||||||
|
short: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Do not ask again for this project',
|
||||||
|
value: 'opt-out',
|
||||||
|
short: 'no (opt out)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function promptGitConnectMultipleUrls(
|
||||||
|
client: Client,
|
||||||
|
remoteUrls: Dictionary<string>
|
||||||
|
) {
|
||||||
|
const staticOptions = [
|
||||||
|
{
|
||||||
|
name: 'No',
|
||||||
|
value: 'no',
|
||||||
|
short: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Do not ask again for this project',
|
||||||
|
value: 'opt-out',
|
||||||
|
short: 'no (opt out)',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let choices = [];
|
||||||
|
for (const url of Object.values(remoteUrls)) {
|
||||||
|
choices.push({
|
||||||
|
name: url,
|
||||||
|
value: url,
|
||||||
|
short: url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
choices = choices.concat(staticOptions);
|
||||||
|
|
||||||
|
return await list(client, {
|
||||||
|
message: 'Do you want to connect a Git repository to your Vercel project?',
|
||||||
|
choices,
|
||||||
|
});
|
||||||
|
}
|
||||||
98
packages/cli/src/util/link/handle-options.ts
Normal file
98
packages/cli/src/util/link/handle-options.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import { Org, Project, ProjectSettings } from '../../types';
|
||||||
|
import Client from '../client';
|
||||||
|
import {
|
||||||
|
connectGitProvider,
|
||||||
|
disconnectGitProvider,
|
||||||
|
formatProvider,
|
||||||
|
RepoInfo,
|
||||||
|
parseRepoUrl,
|
||||||
|
} from '../git/connect-git-provider';
|
||||||
|
import { Output } from '../output';
|
||||||
|
import { getCommandName } from '../pkg-name';
|
||||||
|
import updateProject from '../projects/update-project';
|
||||||
|
|
||||||
|
export async function handleOptions(
|
||||||
|
option: string,
|
||||||
|
client: Client,
|
||||||
|
org: Org,
|
||||||
|
project: Project,
|
||||||
|
settings: ProjectSettings,
|
||||||
|
repoInfo?: RepoInfo
|
||||||
|
) {
|
||||||
|
const { output } = client;
|
||||||
|
if (option === 'no') {
|
||||||
|
skip(output);
|
||||||
|
return;
|
||||||
|
} else if (option === 'opt-out') {
|
||||||
|
optOut(client, project, settings);
|
||||||
|
return;
|
||||||
|
} else if (option !== '') {
|
||||||
|
// Option is "yes" or a URL
|
||||||
|
|
||||||
|
// Ensure parsed url exists
|
||||||
|
if (!repoInfo) {
|
||||||
|
const _repoInfo = parseRepoUrl(option);
|
||||||
|
if (!_repoInfo) {
|
||||||
|
output.debug(`Could not parse repo url ${option}.`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
repoInfo = _repoInfo;
|
||||||
|
}
|
||||||
|
return connect(client, org, project, repoInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function optOut(
|
||||||
|
client: Client,
|
||||||
|
project: Project,
|
||||||
|
settings: ProjectSettings
|
||||||
|
) {
|
||||||
|
settings.skipGitConnectDuringLink = true;
|
||||||
|
await updateProject(client, project.name, settings);
|
||||||
|
client.output
|
||||||
|
.log(`Opted out. You can re-enable this prompt by visiting the Settings > Git page on the
|
||||||
|
dashboard for this Project.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function skip(output: Output) {
|
||||||
|
output.log('Skipping...');
|
||||||
|
output.log(
|
||||||
|
`You can connect a Git repository in the future by running ${getCommandName(
|
||||||
|
'git connect'
|
||||||
|
)}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect(
|
||||||
|
client: Client,
|
||||||
|
org: Org,
|
||||||
|
project: Project,
|
||||||
|
repoInfo: RepoInfo
|
||||||
|
): Promise<number | void> {
|
||||||
|
const { output } = client;
|
||||||
|
const { provider, org: parsedOrg, repo } = repoInfo;
|
||||||
|
const repoPath = `${parsedOrg}/${repo}`;
|
||||||
|
|
||||||
|
output.log('Connecting...');
|
||||||
|
|
||||||
|
if (project.link) {
|
||||||
|
await disconnectGitProvider(client, org, project.id);
|
||||||
|
}
|
||||||
|
const connect = await connectGitProvider(
|
||||||
|
client,
|
||||||
|
org,
|
||||||
|
project.id,
|
||||||
|
provider,
|
||||||
|
repoPath
|
||||||
|
);
|
||||||
|
if (connect !== 1) {
|
||||||
|
output.log(
|
||||||
|
`Connected ${formatProvider(provider)} repository ${chalk.cyan(
|
||||||
|
repoPath
|
||||||
|
)}!`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return connect;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ import { EmojiLabel } from '../emoji';
|
|||||||
import createDeploy from '../deploy/create-deploy';
|
import createDeploy from '../deploy/create-deploy';
|
||||||
import Now, { CreateOptions } from '../index';
|
import Now, { CreateOptions } from '../index';
|
||||||
import { isAPIError } from '../errors-ts';
|
import { isAPIError } from '../errors-ts';
|
||||||
|
import { getRemoteUrls } from '../create-git-meta';
|
||||||
|
import { addGitConnection } from './add-git-connection';
|
||||||
|
|
||||||
export interface SetupAndLinkOptions {
|
export interface SetupAndLinkOptions {
|
||||||
forceDelete?: boolean;
|
forceDelete?: boolean;
|
||||||
@@ -128,6 +130,19 @@ export default async function setupAndLink(
|
|||||||
} else {
|
} else {
|
||||||
const project = projectOrNewProjectName;
|
const project = projectOrNewProjectName;
|
||||||
|
|
||||||
|
const remoteUrls = await getRemoteUrls(join(path, '.git/config'), output);
|
||||||
|
if (remoteUrls && !project.skipGitConnectDuringLink) {
|
||||||
|
const connectGit = await addGitConnection(
|
||||||
|
client,
|
||||||
|
org,
|
||||||
|
project,
|
||||||
|
remoteUrls
|
||||||
|
);
|
||||||
|
if (typeof connectGit === 'number') {
|
||||||
|
return { status: 'error', exitCode: connectGit };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await linkFolderToProject(
|
await linkFolderToProject(
|
||||||
output,
|
output,
|
||||||
path,
|
path,
|
||||||
@@ -241,6 +256,21 @@ export default async function setupAndLink(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const project = await createProject(client, newProjectName);
|
const project = await createProject(client, newProjectName);
|
||||||
|
|
||||||
|
const remoteUrls = await getRemoteUrls(join(path, '.git/config'), output);
|
||||||
|
if (remoteUrls) {
|
||||||
|
const connectGit = await addGitConnection(
|
||||||
|
client,
|
||||||
|
org,
|
||||||
|
project,
|
||||||
|
remoteUrls,
|
||||||
|
settings
|
||||||
|
);
|
||||||
|
if (typeof connectGit === 'number') {
|
||||||
|
return { status: 'error', exitCode: connectGit };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await updateProject(client, project.id, settings);
|
await updateProject(client, project.id, settings);
|
||||||
Object.assign(project, settings);
|
Object.assign(project, settings);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default async function createProject(
|
|||||||
) {
|
) {
|
||||||
const project = await client.fetch<Project>('/v1/projects', {
|
const project = await client.fetch<Project>('/v1/projects', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ name: projectName }),
|
body: { name: projectName },
|
||||||
});
|
});
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Client from '../client';
|
import Client from '../client';
|
||||||
import { ProjectSettings } from '../../types';
|
import type { JSONObject, ProjectSettings } from '../../types';
|
||||||
|
|
||||||
interface ProjectSettingsResponse extends ProjectSettings {
|
interface ProjectSettingsResponse extends ProjectSettings {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,11 +13,14 @@ export default async function updateProject(
|
|||||||
prjNameOrId: string,
|
prjNameOrId: string,
|
||||||
settings: ProjectSettings
|
settings: ProjectSettings
|
||||||
) {
|
) {
|
||||||
|
// `ProjectSettings` is technically compatible with JSONObject
|
||||||
|
const body = settings as JSONObject;
|
||||||
|
|
||||||
const res = await client.fetch<ProjectSettingsResponse>(
|
const res = await client.fetch<ProjectSettingsResponse>(
|
||||||
`/v2/projects/${encodeURIComponent(prjNameOrId)}`,
|
`/v2/projects/${encodeURIComponent(prjNameOrId)}`,
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify(settings),
|
body,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
2
packages/cli/test/fixtures/unit/link-connect-git/multiple-remotes/.gitignore
vendored
Normal file
2
packages/cli/test/fixtures/unit/link-connect-git/multiple-remotes/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
!.vercel
|
||||||
|
.vercel
|
||||||
16
packages/cli/test/fixtures/unit/link-connect-git/multiple-remotes/git/config
generated
vendored
Normal file
16
packages/cli/test/fixtures/unit/link-connect-git/multiple-remotes/git/config
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = false
|
||||||
|
logallrefupdates = true
|
||||||
|
ignorecase = true
|
||||||
|
precomposeunicode = true
|
||||||
|
[remote "origin"]
|
||||||
|
url = https://github.com/user/repo.git
|
||||||
|
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||||
|
[remote "secondary"]
|
||||||
|
url = https://github.com/user2/repo2.git
|
||||||
|
fetch = +refs/heads/*:refs/remotes/secondary/*
|
||||||
|
[remote "gitlab"]
|
||||||
|
url = https://gitlab.com/user/repo.git
|
||||||
|
fetch = +refs/heads/*:refs/remotes/gitlab/*
|
||||||
2
packages/cli/test/fixtures/unit/link-connect-git/single-remote-existing-link/.gitignore
vendored
Normal file
2
packages/cli/test/fixtures/unit/link-connect-git/single-remote-existing-link/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
!.vercel
|
||||||
|
.vercel
|
||||||
13
packages/cli/test/fixtures/unit/link-connect-git/single-remote-existing-link/git/config
generated
vendored
Normal file
13
packages/cli/test/fixtures/unit/link-connect-git/single-remote-existing-link/git/config
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = false
|
||||||
|
logallrefupdates = true
|
||||||
|
ignorecase = true
|
||||||
|
precomposeunicode = true
|
||||||
|
[remote "origin"]
|
||||||
|
url = https://github.com/user2/repo2.git
|
||||||
|
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||||
|
[branch "master"]
|
||||||
|
remote = origin
|
||||||
|
merge = refs/heads/master
|
||||||
2
packages/cli/test/fixtures/unit/link-connect-git/single-remote/.gitignore
vendored
Normal file
2
packages/cli/test/fixtures/unit/link-connect-git/single-remote/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
!.vercel
|
||||||
|
.vercel
|
||||||
13
packages/cli/test/fixtures/unit/link-connect-git/single-remote/git/config
generated
vendored
Normal file
13
packages/cli/test/fixtures/unit/link-connect-git/single-remote/git/config
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = false
|
||||||
|
logallrefupdates = true
|
||||||
|
ignorecase = true
|
||||||
|
precomposeunicode = true
|
||||||
|
[remote "origin"]
|
||||||
|
url = https://github.com/user/repo.git
|
||||||
|
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||||
|
[branch "master"]
|
||||||
|
remote = origin
|
||||||
|
merge = refs/heads/master
|
||||||
@@ -49,6 +49,43 @@ export function useDeployment({
|
|||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDeploymentMissingProjectSettings() {
|
||||||
|
client.scenario.post('/:version/deployments', (_req, res) => {
|
||||||
|
res.status(400).json({
|
||||||
|
error: {
|
||||||
|
code: 'missing_project_settings',
|
||||||
|
message:
|
||||||
|
'The `projectSettings` object is required for new projects, but is missing in the deployment payload',
|
||||||
|
framework: {
|
||||||
|
name: 'Other',
|
||||||
|
slug: null,
|
||||||
|
logo: 'https://raw.githubusercontent.com/vercel/vercel/main/packages/frameworks/logos/other.svg',
|
||||||
|
description: 'No framework or an unoptimized framework.',
|
||||||
|
settings: {
|
||||||
|
installCommand: {
|
||||||
|
placeholder: '`yarn install`, `pnpm install`, or `npm install`',
|
||||||
|
},
|
||||||
|
buildCommand: {
|
||||||
|
placeholder: '`npm run vercel-build` or `npm run build`',
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
devCommand: { placeholder: 'None', value: null },
|
||||||
|
outputDirectory: { placeholder: '`public` if it exists, or `.`' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
projectSettings: {
|
||||||
|
devCommand: null,
|
||||||
|
installCommand: null,
|
||||||
|
buildCommand: null,
|
||||||
|
outputDirectory: null,
|
||||||
|
rootDirectory: null,
|
||||||
|
framework: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
deployments = new Map();
|
deployments = new Map();
|
||||||
deploymentBuilds = new Map();
|
deploymentBuilds = new Map();
|
||||||
|
|||||||
@@ -131,6 +131,70 @@ export const defaultProject = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responds to any GET for a project with a 404.
|
||||||
|
* `useUnknownProject` should always come after `useProject`, if any,
|
||||||
|
* to allow `useProject` responses to still happen.
|
||||||
|
*/
|
||||||
|
export function useUnknownProject() {
|
||||||
|
let project: Project;
|
||||||
|
client.scenario.get(`/v8/projects/:projectNameOrId`, (_req, res) => {
|
||||||
|
res.status(404).send();
|
||||||
|
});
|
||||||
|
client.scenario.post(`/:version/projects`, (req, res) => {
|
||||||
|
const { name } = req.body;
|
||||||
|
project = {
|
||||||
|
...defaultProject,
|
||||||
|
name,
|
||||||
|
id: name,
|
||||||
|
};
|
||||||
|
res.json(project);
|
||||||
|
});
|
||||||
|
client.scenario.post(`/v9/projects/:projectNameOrId/link`, (req, res) => {
|
||||||
|
const { type, repo, org } = req.body;
|
||||||
|
const projName = req.params.projectNameOrId;
|
||||||
|
if (projName !== project.name && projName !== project.id) {
|
||||||
|
return res.status(404).send('Invalid Project name or ID');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(type === 'github' || type === 'gitlab' || type === 'bitbucket') &&
|
||||||
|
(repo === 'user/repo' || repo === 'user2/repo2')
|
||||||
|
) {
|
||||||
|
project.link = {
|
||||||
|
type,
|
||||||
|
repo,
|
||||||
|
repoId: 1010,
|
||||||
|
org,
|
||||||
|
gitCredentialId: '',
|
||||||
|
sourceless: true,
|
||||||
|
createdAt: 1656109539791,
|
||||||
|
updatedAt: 1656109539791,
|
||||||
|
};
|
||||||
|
res.json(project);
|
||||||
|
} else {
|
||||||
|
if (type === 'github') {
|
||||||
|
res.status(400).json({
|
||||||
|
message: `To link a GitHub repository, you need to install the GitHub integration first. (400)\nInstall GitHub App: https://github.com/apps/vercel`,
|
||||||
|
action: 'Install GitHub App',
|
||||||
|
link: 'https://github.com/apps/vercel',
|
||||||
|
repo,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(400).json({
|
||||||
|
code: 'repo_not_found',
|
||||||
|
message: `The repository "${repo}" couldn't be found in your linked ${formatProvider(
|
||||||
|
type
|
||||||
|
)} account.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
client.scenario.patch(`/:version/projects/:projectNameOrId`, (req, res) => {
|
||||||
|
Object.assign(project, req.body);
|
||||||
|
res.json(project);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useProject(project: Partial<Project> = defaultProject) {
|
export function useProject(project: Partial<Project> = defaultProject) {
|
||||||
client.scenario.get(`/v8/projects/${project.name}`, (_req, res) => {
|
client.scenario.get(`/v8/projects/${project.name}`, (_req, res) => {
|
||||||
res.json(project);
|
res.json(project);
|
||||||
@@ -138,6 +202,10 @@ export function useProject(project: Partial<Project> = defaultProject) {
|
|||||||
client.scenario.get(`/v8/projects/${project.id}`, (_req, res) => {
|
client.scenario.get(`/v8/projects/${project.id}`, (_req, res) => {
|
||||||
res.json(project);
|
res.json(project);
|
||||||
});
|
});
|
||||||
|
client.scenario.patch(`/:version/projects/${project.id}`, (req, res) => {
|
||||||
|
Object.assign(project, req.body);
|
||||||
|
res.json(project);
|
||||||
|
});
|
||||||
client.scenario.get(
|
client.scenario.get(
|
||||||
`/v6/projects/${project.id}/system-env-values`,
|
`/v6/projects/${project.id}/system-env-values`,
|
||||||
(_req, res) => {
|
(_req, res) => {
|
||||||
@@ -183,7 +251,7 @@ export function useProject(project: Partial<Project> = defaultProject) {
|
|||||||
res.json(envs);
|
res.json(envs);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
client.scenario.post(`/v4/projects/${project.id}/link`, (req, res) => {
|
client.scenario.post(`/v9/projects/${project.id}/link`, (req, res) => {
|
||||||
const { type, repo, org } = req.body;
|
const { type, repo, org } = req.body;
|
||||||
if (
|
if (
|
||||||
(type === 'github' || type === 'gitlab' || type === 'bitbucket') &&
|
(type === 'github' || type === 'gitlab' || type === 'bitbucket') &&
|
||||||
@@ -218,7 +286,7 @@ export function useProject(project: Partial<Project> = defaultProject) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
client.scenario.delete(`/v4/projects/${project.id}/link`, (_req, res) => {
|
client.scenario.delete(`/v9/projects/${project.id}/link`, (_req, res) => {
|
||||||
if (project.link) {
|
if (project.link) {
|
||||||
project.link = undefined;
|
project.link = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ describe('git', () => {
|
|||||||
await expect(client.stderr).toOutput('Found project');
|
await expect(client.stderr).toOutput('Found project');
|
||||||
client.stdin.write('y\n');
|
client.stdin.write('y\n');
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Do you want to connect "origin" to your Vercel project?'
|
||||||
|
);
|
||||||
|
client.stdin.write('n\n');
|
||||||
|
|
||||||
await expect(client.stderr).toOutput(
|
await expect(client.stderr).toOutput(
|
||||||
`Connecting Git remote: https://github.com/user/repo.git`
|
`Connecting Git remote: https://github.com/user/repo.git`
|
||||||
);
|
);
|
||||||
|
|||||||
310
packages/cli/test/unit/commands/link.test.ts
Normal file
310
packages/cli/test/unit/commands/link.test.ts
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import link from '../../../src/commands/link';
|
||||||
|
import { useUser } from '../../mocks/user';
|
||||||
|
import { useTeams } from '../../mocks/team';
|
||||||
|
import {
|
||||||
|
defaultProject,
|
||||||
|
useUnknownProject,
|
||||||
|
useProject,
|
||||||
|
} from '../../mocks/project';
|
||||||
|
import { client } from '../../mocks/client';
|
||||||
|
import { useDeploymentMissingProjectSettings } from '../../mocks/deployment';
|
||||||
|
import { Project } from '../../../src/types';
|
||||||
|
|
||||||
|
describe('link', () => {
|
||||||
|
describe('git prompt', () => {
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
const fixture = (name: string) =>
|
||||||
|
join(__dirname, '../../fixtures/unit/link-connect-git', name);
|
||||||
|
|
||||||
|
it('should prompt to connect a new project with a single remote', async () => {
|
||||||
|
const cwd = fixture('single-remote');
|
||||||
|
try {
|
||||||
|
process.chdir(cwd);
|
||||||
|
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||||
|
useUser();
|
||||||
|
useUnknownProject();
|
||||||
|
useDeploymentMissingProjectSettings();
|
||||||
|
useTeams('team_dummy');
|
||||||
|
const linkPromise = link(client);
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput('Set up');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
await expect(client.stderr).toOutput('Which scope');
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput('Link to existing project?');
|
||||||
|
client.stdin.write('n\n');
|
||||||
|
await expect(client.stderr).toOutput('What’s your project’s name?');
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'In which directory is your code located?'
|
||||||
|
);
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput('Want to modify these settings?');
|
||||||
|
client.stdin.write('n\n');
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Found local Git remote "origin": https://github.com/user/repo.git'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Do you want to connect "origin" to your Vercel project?'
|
||||||
|
);
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Connected GitHub repository user/repo!'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Linked to');
|
||||||
|
await expect(linkPromise).resolves.toEqual(0);
|
||||||
|
} finally {
|
||||||
|
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prompt to connect an existing project with a single remote to git', async () => {
|
||||||
|
const cwd = fixture('single-remote');
|
||||||
|
try {
|
||||||
|
process.chdir(cwd);
|
||||||
|
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||||
|
useUser();
|
||||||
|
useProject({
|
||||||
|
...defaultProject,
|
||||||
|
name: 'single-remote',
|
||||||
|
id: 'single-remote',
|
||||||
|
});
|
||||||
|
useTeams('team_dummy');
|
||||||
|
const linkPromise = link(client);
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput('Set up');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
await expect(client.stderr).toOutput('Which scope');
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput('Found project');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Found local Git remote "origin": https://github.com/user/repo.git'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Do you want to connect "origin" to your Vercel project?'
|
||||||
|
);
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Connected GitHub repository user/repo!'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Linked to');
|
||||||
|
|
||||||
|
await expect(linkPromise).resolves.toEqual(0);
|
||||||
|
} finally {
|
||||||
|
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should prompt to replace a connected repository if there is one remote', async () => {
|
||||||
|
const cwd = fixture('single-remote-existing-link');
|
||||||
|
try {
|
||||||
|
process.chdir(cwd);
|
||||||
|
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||||
|
useUser();
|
||||||
|
const project = useProject({
|
||||||
|
...defaultProject,
|
||||||
|
name: 'single-remote-existing-link',
|
||||||
|
id: 'single-remote-existing-link',
|
||||||
|
});
|
||||||
|
useTeams('team_dummy');
|
||||||
|
project.project.link = {
|
||||||
|
type: 'github',
|
||||||
|
org: 'user',
|
||||||
|
repo: 'repo',
|
||||||
|
repoId: 1010,
|
||||||
|
gitCredentialId: '',
|
||||||
|
sourceless: true,
|
||||||
|
createdAt: 1656109539791,
|
||||||
|
updatedAt: 1656109539791,
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkPromise = link(client);
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput('Set up');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
await expect(client.stderr).toOutput('Which scope');
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput('Found project');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`Found Git remote URL https://github.com/user2/repo2.git, which is different from the connected GitHub repository user/repo.`
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Do you want to replace it?');
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Connected GitHub repository user2/repo2!'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Linked to');
|
||||||
|
|
||||||
|
await expect(linkPromise).resolves.toEqual(0);
|
||||||
|
} finally {
|
||||||
|
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should prompt to connect an existing project with multiple remotes', async () => {
|
||||||
|
const cwd = fixture('multiple-remotes');
|
||||||
|
try {
|
||||||
|
process.chdir(cwd);
|
||||||
|
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||||
|
|
||||||
|
useUser();
|
||||||
|
useProject({
|
||||||
|
...defaultProject,
|
||||||
|
name: 'multiple-remotes',
|
||||||
|
id: 'multiple-remotes',
|
||||||
|
});
|
||||||
|
useTeams('team_dummy');
|
||||||
|
|
||||||
|
const linkPromise = link(client);
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput('Set up');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
await expect(client.stderr).toOutput('Which scope');
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput('Found project');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
`> Do you want to connect a Git repository to your Vercel project?`
|
||||||
|
);
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Connected GitHub repository user/repo!'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput('Linked to');
|
||||||
|
|
||||||
|
await expect(linkPromise).resolves.toEqual(0);
|
||||||
|
} finally {
|
||||||
|
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should not prompt to replace a connected repository if there is more than one remote', async () => {
|
||||||
|
const cwd = fixture('multiple-remotes');
|
||||||
|
try {
|
||||||
|
process.chdir(cwd);
|
||||||
|
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||||
|
|
||||||
|
useUser();
|
||||||
|
const project = useProject({
|
||||||
|
...defaultProject,
|
||||||
|
name: 'multiple-remotes',
|
||||||
|
id: 'multiple-remotes',
|
||||||
|
});
|
||||||
|
useTeams('team_dummy');
|
||||||
|
project.project.link = {
|
||||||
|
type: 'github',
|
||||||
|
org: 'user',
|
||||||
|
repo: 'repo',
|
||||||
|
repoId: 1010,
|
||||||
|
gitCredentialId: '',
|
||||||
|
sourceless: true,
|
||||||
|
createdAt: 1656109539791,
|
||||||
|
updatedAt: 1656109539791,
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkPromise = link(client);
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput('Set up');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
await expect(client.stderr).toOutput('Which scope');
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput('Found project');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
|
||||||
|
expect(client.stderr).not.toOutput('Found multiple Git remote URLs');
|
||||||
|
await expect(client.stderr).toOutput('Linked to');
|
||||||
|
|
||||||
|
await expect(linkPromise).resolves.toEqual(0);
|
||||||
|
} finally {
|
||||||
|
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should set a project setting if user opts out', async () => {
|
||||||
|
const cwd = fixture('single-remote');
|
||||||
|
try {
|
||||||
|
process.chdir(cwd);
|
||||||
|
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||||
|
|
||||||
|
useUser();
|
||||||
|
useProject({
|
||||||
|
...defaultProject,
|
||||||
|
name: 'single-remote',
|
||||||
|
id: 'single-remote',
|
||||||
|
});
|
||||||
|
useTeams('team_dummy');
|
||||||
|
const linkPromise = link(client);
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput('Set up');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
await expect(client.stderr).toOutput('Which scope');
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput('Found project');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Found local Git remote "origin": https://github.com/user/repo.git'
|
||||||
|
);
|
||||||
|
await expect(client.stderr).toOutput(
|
||||||
|
'Do you want to connect "origin" to your Vercel project?'
|
||||||
|
);
|
||||||
|
client.stdin.write('\x1B[B'); // Down arrow
|
||||||
|
client.stdin.write('\x1B[B');
|
||||||
|
client.stdin.write('\r'); // Opt out
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput(`Opted out.`);
|
||||||
|
await expect(client.stderr).toOutput('Linked to');
|
||||||
|
await expect(linkPromise).resolves.toEqual(0);
|
||||||
|
|
||||||
|
const newProjectData: Project = await client.fetch(
|
||||||
|
`/v8/projects/single-remote`
|
||||||
|
);
|
||||||
|
expect(newProjectData.skipGitConnectDuringLink).toBeTruthy();
|
||||||
|
} finally {
|
||||||
|
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should not prompt to connect git if the project has skipGitConnectDuringLink property', async () => {
|
||||||
|
const cwd = fixture('single-remote');
|
||||||
|
try {
|
||||||
|
process.chdir(cwd);
|
||||||
|
await fs.rename(join(cwd, 'git'), join(cwd, '.git'));
|
||||||
|
|
||||||
|
useUser();
|
||||||
|
const project = useProject({
|
||||||
|
...defaultProject,
|
||||||
|
name: 'single-remote',
|
||||||
|
id: 'single-remote',
|
||||||
|
});
|
||||||
|
useTeams('team_dummy');
|
||||||
|
project.project.skipGitConnectDuringLink = true;
|
||||||
|
const linkPromise = link(client);
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput('Set up');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
await expect(client.stderr).toOutput('Which scope');
|
||||||
|
client.stdin.write('\r');
|
||||||
|
await expect(client.stderr).toOutput('Found project');
|
||||||
|
client.stdin.write('y\n');
|
||||||
|
|
||||||
|
expect(client.stderr).not.toOutput('Found local Git remote "origin"');
|
||||||
|
|
||||||
|
await expect(client.stderr).toOutput('Linked to');
|
||||||
|
await expect(linkPromise).resolves.toEqual(0);
|
||||||
|
} finally {
|
||||||
|
await fs.rename(join(cwd, '.git'), join(cwd, 'git'));
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -58,92 +58,92 @@ describe('getRemoteUrls', () => {
|
|||||||
|
|
||||||
describe('parseRepoUrl', () => {
|
describe('parseRepoUrl', () => {
|
||||||
it('should be null when a url does not match the regex', () => {
|
it('should be null when a url does not match the regex', () => {
|
||||||
const parsedUrl = parseRepoUrl('https://examplecom/foo');
|
const repoInfo = parseRepoUrl('https://examplecom/foo');
|
||||||
expect(parsedUrl).toBeNull();
|
expect(repoInfo).toBeNull();
|
||||||
});
|
});
|
||||||
it('should be null when a url does not contain org and repo data', () => {
|
it('should be null when a url does not contain org and repo data', () => {
|
||||||
const parsedUrl = parseRepoUrl('https://github.com/borked');
|
const repoInfo = parseRepoUrl('https://github.com/borked');
|
||||||
expect(parsedUrl).toBeNull();
|
expect(repoInfo).toBeNull();
|
||||||
});
|
});
|
||||||
it('should parse url with a period in the repo name', () => {
|
it('should parse url with a period in the repo name', () => {
|
||||||
const parsedUrl = parseRepoUrl('https://github.com/vercel/next.js');
|
const repoInfo = parseRepoUrl('https://github.com/vercel/next.js');
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('github');
|
expect(repoInfo?.provider).toEqual('github');
|
||||||
expect(parsedUrl?.org).toEqual('vercel');
|
expect(repoInfo?.org).toEqual('vercel');
|
||||||
expect(parsedUrl?.repo).toEqual('next.js');
|
expect(repoInfo?.repo).toEqual('next.js');
|
||||||
});
|
});
|
||||||
it('should parse url that ends with .git', () => {
|
it('should parse url that ends with .git', () => {
|
||||||
const parsedUrl = parseRepoUrl('https://github.com/vercel/next.js.git');
|
const repoInfo = parseRepoUrl('https://github.com/vercel/next.js.git');
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('github');
|
expect(repoInfo?.provider).toEqual('github');
|
||||||
expect(parsedUrl?.org).toEqual('vercel');
|
expect(repoInfo?.org).toEqual('vercel');
|
||||||
expect(parsedUrl?.repo).toEqual('next.js');
|
expect(repoInfo?.repo).toEqual('next.js');
|
||||||
});
|
});
|
||||||
it('should parse github https url', () => {
|
it('should parse github https url', () => {
|
||||||
const parsedUrl = parseRepoUrl('https://github.com/vercel/vercel.git');
|
const repoInfo = parseRepoUrl('https://github.com/vercel/vercel.git');
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('github');
|
expect(repoInfo?.provider).toEqual('github');
|
||||||
expect(parsedUrl?.org).toEqual('vercel');
|
expect(repoInfo?.org).toEqual('vercel');
|
||||||
expect(parsedUrl?.repo).toEqual('vercel');
|
expect(repoInfo?.repo).toEqual('vercel');
|
||||||
});
|
});
|
||||||
it('should parse github https url without the .git suffix', () => {
|
it('should parse github https url without the .git suffix', () => {
|
||||||
const parsedUrl = parseRepoUrl('https://github.com/vercel/vercel');
|
const repoInfo = parseRepoUrl('https://github.com/vercel/vercel');
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('github');
|
expect(repoInfo?.provider).toEqual('github');
|
||||||
expect(parsedUrl?.org).toEqual('vercel');
|
expect(repoInfo?.org).toEqual('vercel');
|
||||||
expect(parsedUrl?.repo).toEqual('vercel');
|
expect(repoInfo?.repo).toEqual('vercel');
|
||||||
});
|
});
|
||||||
it('should parse github git url', () => {
|
it('should parse github git url', () => {
|
||||||
const parsedUrl = parseRepoUrl('git://github.com/vercel/vercel.git');
|
const repoInfo = parseRepoUrl('git://github.com/vercel/vercel.git');
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('github');
|
expect(repoInfo?.provider).toEqual('github');
|
||||||
expect(parsedUrl?.org).toEqual('vercel');
|
expect(repoInfo?.org).toEqual('vercel');
|
||||||
expect(parsedUrl?.repo).toEqual('vercel');
|
expect(repoInfo?.repo).toEqual('vercel');
|
||||||
});
|
});
|
||||||
it('should parse github ssh url', () => {
|
it('should parse github ssh url', () => {
|
||||||
const parsedUrl = parseRepoUrl('git@github.com:vercel/vercel.git');
|
const repoInfo = parseRepoUrl('git@github.com:vercel/vercel.git');
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('github');
|
expect(repoInfo?.provider).toEqual('github');
|
||||||
expect(parsedUrl?.org).toEqual('vercel');
|
expect(repoInfo?.org).toEqual('vercel');
|
||||||
expect(parsedUrl?.repo).toEqual('vercel');
|
expect(repoInfo?.repo).toEqual('vercel');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse gitlab https url', () => {
|
it('should parse gitlab https url', () => {
|
||||||
const parsedUrl = parseRepoUrl(
|
const repoInfo = parseRepoUrl(
|
||||||
'https://gitlab.com/gitlab-examples/knative-kotlin-app.git'
|
'https://gitlab.com/gitlab-examples/knative-kotlin-app.git'
|
||||||
);
|
);
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('gitlab');
|
expect(repoInfo?.provider).toEqual('gitlab');
|
||||||
expect(parsedUrl?.org).toEqual('gitlab-examples');
|
expect(repoInfo?.org).toEqual('gitlab-examples');
|
||||||
expect(parsedUrl?.repo).toEqual('knative-kotlin-app');
|
expect(repoInfo?.repo).toEqual('knative-kotlin-app');
|
||||||
});
|
});
|
||||||
it('should parse gitlab ssh url', () => {
|
it('should parse gitlab ssh url', () => {
|
||||||
const parsedUrl = parseRepoUrl(
|
const repoInfo = parseRepoUrl(
|
||||||
'git@gitlab.com:gitlab-examples/knative-kotlin-app.git'
|
'git@gitlab.com:gitlab-examples/knative-kotlin-app.git'
|
||||||
);
|
);
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('gitlab');
|
expect(repoInfo?.provider).toEqual('gitlab');
|
||||||
expect(parsedUrl?.org).toEqual('gitlab-examples');
|
expect(repoInfo?.org).toEqual('gitlab-examples');
|
||||||
expect(parsedUrl?.repo).toEqual('knative-kotlin-app');
|
expect(repoInfo?.repo).toEqual('knative-kotlin-app');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse bitbucket https url', () => {
|
it('should parse bitbucket https url', () => {
|
||||||
const parsedUrl = parseRepoUrl(
|
const repoInfo = parseRepoUrl(
|
||||||
'https://bitbucket.org/atlassianlabs/maven-project-example.git'
|
'https://bitbucket.org/atlassianlabs/maven-project-example.git'
|
||||||
);
|
);
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('bitbucket');
|
expect(repoInfo?.provider).toEqual('bitbucket');
|
||||||
expect(parsedUrl?.org).toEqual('atlassianlabs');
|
expect(repoInfo?.org).toEqual('atlassianlabs');
|
||||||
expect(parsedUrl?.repo).toEqual('maven-project-example');
|
expect(repoInfo?.repo).toEqual('maven-project-example');
|
||||||
});
|
});
|
||||||
it('should parse bitbucket ssh url', () => {
|
it('should parse bitbucket ssh url', () => {
|
||||||
const parsedUrl = parseRepoUrl(
|
const repoInfo = parseRepoUrl(
|
||||||
'git@bitbucket.org:atlassianlabs/maven-project-example.git'
|
'git@bitbucket.org:atlassianlabs/maven-project-example.git'
|
||||||
);
|
);
|
||||||
expect(parsedUrl).toBeDefined();
|
expect(repoInfo).toBeDefined();
|
||||||
expect(parsedUrl?.provider).toEqual('bitbucket');
|
expect(repoInfo?.provider).toEqual('bitbucket');
|
||||||
expect(parsedUrl?.org).toEqual('atlassianlabs');
|
expect(repoInfo?.org).toEqual('atlassianlabs');
|
||||||
expect(parsedUrl?.repo).toEqual('maven-project-example');
|
expect(repoInfo?.repo).toEqual('maven-project-example');
|
||||||
});
|
});
|
||||||
it('should parse url without a scheme', () => {
|
it('should parse url without a scheme', () => {
|
||||||
const parsedUrl = parseRepoUrl('github.com/user/repo');
|
const parsedUrl = parseRepoUrl('github.com/user/repo');
|
||||||
|
|||||||
Reference in New Issue
Block a user