[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:
Matthew Stanciu
2022-08-11 15:49:36 -07:00
committed by GitHub
parent 0fcf172a10
commit c98c9996bf
20 changed files with 878 additions and 87 deletions

View 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('Whats your projects 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);
}
});
});
});