diff --git a/package-lock.json b/package-lock.json index cb136c299d3..f675d93f317 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6255,6 +6255,7 @@ }, "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { "version": "1.1.0", + "extraneous": true, "inBundle": true, "license": "MIT" }, diff --git a/src/utils/init/config-manual.ts b/src/utils/init/config-manual.ts index 7b8c6779a3e..366f2300c46 100644 --- a/src/utils/init/config-manual.ts +++ b/src/utils/init/config-manual.ts @@ -86,9 +86,9 @@ export default async function configManual({ const deployKey = await createDeployKey({ api }) await addDeployKey(deployKey) - const repoPath = await getRepoPath({ repoData }) + const repoPath = repoData.repo ?? (await getRepoPath({ repoData })) const repo = { - provider: 'manual', + provider: repoData.provider ?? 'manual', repo_path: repoPath, repo_branch: repoData.branch, allowed_branches: [repoData.branch], diff --git a/tests/unit/utils/get-repo-data.test.ts b/tests/unit/utils/get-repo-data.test.ts new file mode 100644 index 00000000000..6433a1ffae1 --- /dev/null +++ b/tests/unit/utils/get-repo-data.test.ts @@ -0,0 +1,143 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import getRepoData from '../../../src/utils/get-repo-data.js' + +const mockGitConfig = vi.fn() +const mockFindUp = vi.fn() +const mockGitRepoInfo = vi.fn() + +vi.mock('gitconfiglocal', () => ({ + default: (workingDir: string, cb: (err: Error | null, config: unknown) => void) => { + try { + cb(null, mockGitConfig(workingDir)) + } catch (err) { + cb(err as Error, null) + } + }, +})) + +vi.mock('find-up', () => ({ + findUp: (...args: unknown[]): unknown => mockFindUp(...args), +})) + +vi.mock('git-repo-info', () => ({ + default: (): unknown => mockGitRepoInfo(), +})) + +vi.mock('../../../src/utils/command-helpers.js', () => ({ + log: vi.fn(), +})) + +describe('getRepoData', () => { + beforeEach(() => { + vi.clearAllMocks() + mockFindUp.mockResolvedValue('/test/.git') + mockGitRepoInfo.mockReturnValue({ branch: 'main' }) + }) + + it('parses GitHub SSH URLs', async () => { + mockGitConfig.mockReturnValue({ remote: { origin: { url: 'git@github.com:ownername/test.git' } } }) + + const result = await getRepoData({ workingDir: '/test' }) + + expect(result).toEqual({ + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'git@github.com:ownername/test.git', + branch: 'main', + provider: 'github', + httpsUrl: 'https://github.com/ownername/test', + }) + }) + + it('parses GitLab SSH URLs', async () => { + mockGitConfig.mockReturnValue({ remote: { origin: { url: 'git@gitlab.com:ownername/test.git' } } }) + + const result = await getRepoData({ workingDir: '/test' }) + + expect(result).toEqual({ + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'git@gitlab.com:ownername/test.git', + branch: 'main', + provider: 'gitlab', + httpsUrl: 'https://gitlab.com/ownername/test', + }) + }) + + it('parses GitHub HTTPS URLs', async () => { + mockGitConfig.mockReturnValue({ remote: { origin: { url: 'https://github.com/ownername/test.git' } } }) + + const result = await getRepoData({ workingDir: '/test' }) + + expect(result).toMatchObject({ + provider: 'github', + repo: 'ownername/test', + httpsUrl: 'https://github.com/ownername/test', + }) + }) + + it('parses GitLab HTTPS URLs', async () => { + mockGitConfig.mockReturnValue({ remote: { origin: { url: 'https://gitlab.com/ownername/test.git' } } }) + + const result = await getRepoData({ workingDir: '/test' }) + + expect(result).toMatchObject({ + provider: 'gitlab', + repo: 'ownername/test', + httpsUrl: 'https://gitlab.com/ownername/test', + }) + }) + + it('uses host as provider for unknown Git hosts', async () => { + mockGitConfig.mockReturnValue({ + remote: { origin: { url: 'git@custom-git.example.com:user/test.git' } }, + }) + + const result = await getRepoData({ workingDir: '/test' }) + + expect(result).toMatchObject({ + provider: 'custom-git.example.com', + repo: 'user/test', + httpsUrl: 'https://custom-git.example.com/user/test', + }) + }) + + it('uses the specified remote name when provided', async () => { + mockGitConfig.mockReturnValue({ + remote: { + origin: { url: 'git@github.com:owner/origin-repo.git' }, + upstream: { url: 'git@gitlab.com:owner/upstream-repo.git' }, + }, + }) + + const result = await getRepoData({ workingDir: '/test', remoteName: 'upstream' }) + + expect(result).toMatchObject({ + provider: 'gitlab', + repo: 'owner/upstream-repo', + }) + }) + + it('returns an error when no Git remote is found', async () => { + mockFindUp.mockResolvedValue(undefined) + mockGitConfig.mockReturnValue({ remote: {} }) + + const result = await getRepoData({ workingDir: '/test' }) + + expect(result).toEqual({ error: 'No Git remote found' }) + }) + + it('returns an error when the requested remote is not defined', async () => { + mockGitConfig.mockReturnValue({ remote: { origin: { url: 'git@github.com:owner/repo.git' } } }) + + const result = await getRepoData({ workingDir: '/test', remoteName: 'missing' }) + + expect(result).toEqual({ + error: + 'The specified remote "missing" is not defined in Git repo. Please use --git-remote-name flag to specify a remote.', + }) + }) +}) diff --git a/tests/unit/utils/init/config-manual.test.ts b/tests/unit/utils/init/config-manual.test.ts new file mode 100644 index 00000000000..0d2bd7edadc --- /dev/null +++ b/tests/unit/utils/init/config-manual.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest' +import type { RepoData } from '../../../../src/utils/get-repo-data.js' +import type { NetlifyAPI } from '@netlify/api' +import type BaseCommand from '../../../../src/commands/base-command.js' + +const mockPrompt = vi.fn() +const mockLog = vi.fn() +const mockExit = vi.fn() +const mockCreateDeployKey = vi.fn() +const mockGetBuildSettings = vi.fn() +const mockSaveNetlifyToml = vi.fn() +const mockSetupSite = vi.fn() + +vi.mock('inquirer', () => ({ + default: { + prompt: mockPrompt, + }, +})) + +vi.mock('../../../../src/utils/command-helpers.js', () => ({ + log: mockLog, + exit: mockExit, +})) + +vi.mock('../../../../src/utils/init/utils.js', () => ({ + createDeployKey: mockCreateDeployKey, + getBuildSettings: mockGetBuildSettings, + saveNetlifyToml: mockSaveNetlifyToml, + setupSite: mockSetupSite, +})) + +describe('config-manual', () => { + let mockApi: Partial + let mockCommand: Pick + + beforeEach(() => { + vi.clearAllMocks() + + mockApi = {} + mockCommand = { + netlify: { + api: mockApi as NetlifyAPI, + cachedConfig: { configPath: '/test/netlify.toml' } as BaseCommand['netlify']['cachedConfig'], + config: { plugins: [] } as unknown as BaseCommand['netlify']['config'], + repositoryRoot: '/test', + } as BaseCommand['netlify'], + } + + mockPrompt.mockResolvedValue({ + sshKeyAdded: true, + repoPath: 'git@gitlab.com:test/repo.git', + deployHookAdded: true, + }) + + mockCreateDeployKey.mockResolvedValue({ id: 'key-123', public_key: 'ssh-rsa test' }) + mockGetBuildSettings.mockResolvedValue({ + baseDir: '', + buildCmd: 'npm run build', + buildDir: 'dist', + functionsDir: 'functions', + pluginsToInstall: [], + }) + mockSaveNetlifyToml.mockResolvedValue(undefined) + mockSetupSite.mockResolvedValue({ deploy_hook: 'https://api.netlify.com/hooks/test' }) + }) + + describe('GitLab repository configuration', () => { + it('should use provider from repoData for GitLab repos', async () => { + const configManual = (await import('../../../../src/utils/init/config-manual.js')).default + + const repoData: RepoData = { + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'git@gitlab.com:ownername/test.git', + branch: 'main', + provider: 'gitlab', + httpsUrl: 'https://gitlab.com/ownername/test', + } + + await configManual({ + command: mockCommand as BaseCommand, + repoData, + siteId: 'site-123', + }) + + const setupCall = mockSetupSite.mock.calls[0][0] as { repo: { provider: string; repo_path: string } } + expect(setupCall.repo.provider).toBe('gitlab') + expect(setupCall.repo.repo_path).toBe('ownername/test') + }) + + it('should use repo path (owner/name format) instead of SSH URL for GitLab', async () => { + const configManual = (await import('../../../../src/utils/init/config-manual.js')).default + + const repoData: RepoData = { + name: 'test', + owner: 'ownername', + repo: 'ownername/test', + url: 'git@gitlab.com:ownername/test.git', + branch: 'main', + provider: 'gitlab', + httpsUrl: 'https://gitlab.com/ownername/test', + } + + await configManual({ + command: mockCommand as BaseCommand, + repoData, + siteId: 'site-123', + }) + + const setupSiteCall = mockSetupSite.mock.calls[0][0] as { + repo: { repo_path: string } + } + expect(setupSiteCall.repo.repo_path).toBe('ownername/test') + expect(setupSiteCall.repo.repo_path).not.toBe('git@gitlab.com:ownername/test.git') + }) + + it('should fallback to manual provider when provider is null', async () => { + const configManual = (await import('../../../../src/utils/init/config-manual.js')).default + + const repoData: RepoData = { + name: 'test', + owner: 'user', + repo: 'user/test', + url: 'git@custom.com:user/test.git', + branch: 'main', + provider: null, + httpsUrl: 'https://custom.com/user/test', + } + + await configManual({ + command: mockCommand as BaseCommand, + repoData, + siteId: 'site-123', + }) + + const setupCall = mockSetupSite.mock.calls[0][0] as { repo: { provider: string } } + expect(setupCall.repo.provider).toBe('manual') + }) + }) + + describe('GitHub repository configuration', () => { + it('should use provider from repoData for GitHub repos', async () => { + const configManual = (await import('../../../../src/utils/init/config-manual.js')).default + + const repoData: RepoData = { + name: 'test', + owner: 'user', + repo: 'user/test', + url: 'git@github.com:user/test.git', + branch: 'main', + provider: 'github', + httpsUrl: 'https://github.com/user/test', + } + + await configManual({ + command: mockCommand as BaseCommand, + repoData, + siteId: 'site-123', + }) + + const setupCall = mockSetupSite.mock.calls[0][0] as { repo: { provider: string; repo_path: string } } + expect(setupCall.repo.provider).toBe('github') + expect(setupCall.repo.repo_path).toBe('user/test') + }) + }) +})