diff --git a/apps/web/src/components/cloud-agent-next/utils/git-utils.test.ts b/apps/web/src/components/cloud-agent-next/utils/git-utils.test.ts index 2a1d7594203..fca344787d6 100644 --- a/apps/web/src/components/cloud-agent-next/utils/git-utils.test.ts +++ b/apps/web/src/components/cloud-agent-next/utils/git-utils.test.ts @@ -3,7 +3,6 @@ import { buildRepoBrowseUrl, detectGitPlatform, extractRepoFromGitUrl, - buildPrepareSessionRepoParams, findAllGitPlatformUrls, } from './git-utils'; @@ -217,34 +216,6 @@ describe('extractRepoFromGitUrl', () => { }); }); -describe('buildPrepareSessionRepoParams', () => { - test('github platform with repo', () => { - expect(buildPrepareSessionRepoParams({ repo: 'owner/repo', platform: 'github' })).toEqual({ - githubRepo: 'owner/repo', - }); - }); - - test('gitlab platform with repo', () => { - expect(buildPrepareSessionRepoParams({ repo: 'group/project', platform: 'gitlab' })).toEqual({ - gitlabProject: 'group/project', - }); - }); - - test('bitbucket platform requires structured repository identity', () => { - expect( - buildPrepareSessionRepoParams({ repo: 'workspace/repo', platform: 'bitbucket' }) - ).toBeNull(); - }); - - test('null repo returns null', () => { - expect(buildPrepareSessionRepoParams({ repo: null, platform: 'github' })).toBeNull(); - }); - - test('empty string repo returns null', () => { - expect(buildPrepareSessionRepoParams({ repo: '', platform: 'github' })).toBeNull(); - }); -}); - describe('findAllGitPlatformUrls', () => { test('extracts GitHub URL from plain text', () => { expect(findAllGitPlatformUrls('Fix https://github.com/owner/repo/issues/42 please')).toEqual([ diff --git a/apps/web/src/components/cloud-agent-next/utils/git-utils.ts b/apps/web/src/components/cloud-agent-next/utils/git-utils.ts index 7c163d38dcd..12ad676add0 100644 --- a/apps/web/src/components/cloud-agent-next/utils/git-utils.ts +++ b/apps/web/src/components/cloud-agent-next/utils/git-utils.ts @@ -5,8 +5,6 @@ * This module has no dependencies on other cloud-agent-next modules to avoid circular imports. */ -import { PLATFORM } from '@/lib/integrations/core/constants'; - /** * Extract owner/repo, group/project, or workspace/repo from a git URL * @@ -65,26 +63,6 @@ export function extractRepoFromGitUrl(gitUrl: string | null | undefined): string export type GitPlatform = 'github' | 'gitlab' | 'bitbucket'; -export function buildPrepareSessionRepoParams(options: { - repo?: string | null; - platform: GitPlatform; -}): { githubRepo?: string; gitlabProject?: string } | null { - const repo = options.repo?.trim(); - if (!repo) return null; - - switch (options.platform) { - case PLATFORM.GITHUB: - return { githubRepo: repo }; - case PLATFORM.GITLAB: - return { gitlabProject: repo }; - case 'bitbucket': - return null; - default: - options.platform satisfies never; - return null; - } -} - export function buildRepoBrowseUrl(gitUrl: string | null | undefined): string | undefined { if (!gitUrl) return undefined; diff --git a/apps/web/src/components/cloud-agent/utils/git-utils.test.ts b/apps/web/src/components/cloud-agent/utils/git-utils.test.ts deleted file mode 100644 index 73cb5036295..00000000000 --- a/apps/web/src/components/cloud-agent/utils/git-utils.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { extractRepoFromGitUrl, buildPrepareSessionRepoParams } from './git-utils'; - -describe('extractRepoFromGitUrl', () => { - describe('HTTPS URLs', () => { - it('extracts owner/repo from GitHub HTTPS URL', () => { - expect(extractRepoFromGitUrl('https://github.com/owner/repo')).toBe('owner/repo'); - }); - - it('extracts owner/repo from GitHub HTTPS URL with .git suffix', () => { - expect(extractRepoFromGitUrl('https://github.com/owner/repo.git')).toBe('owner/repo'); - }); - - it('extracts group/project from GitLab HTTPS URL', () => { - expect(extractRepoFromGitUrl('https://gitlab.com/group/project')).toBe('group/project'); - }); - - it('extracts group/project from GitLab HTTPS URL with .git suffix', () => { - expect(extractRepoFromGitUrl('https://gitlab.com/group/project.git')).toBe('group/project'); - }); - - it('extracts only first two segments from GitLab HTTPS URL without .git suffix', () => { - // Without .git suffix, we can't distinguish between a clone URL and a web URL - // so we conservatively take only the first two segments (like GitHub) - expect(extractRepoFromGitUrl('https://gitlab.com/group/subgroup/project')).toBe( - 'group/subgroup' - ); - }); - - it('extracts nested group/subgroup/project from GitLab HTTPS URL with .git suffix', () => { - expect(extractRepoFromGitUrl('https://gitlab.com/group/subgroup/project.git')).toBe( - 'group/subgroup/project' - ); - }); - - it('extracts deeply nested GitLab project path', () => { - expect(extractRepoFromGitUrl('https://gitlab.com/a/b/c/d/project.git')).toBe( - 'a/b/c/d/project' - ); - }); - - it('extracts only owner/repo from GitHub tree URL (non-clone URL)', () => { - expect(extractRepoFromGitUrl('https://github.com/owner/repo/tree/main')).toBe('owner/repo'); - }); - - it('extracts only owner/repo from GitHub blob URL (non-clone URL)', () => { - expect(extractRepoFromGitUrl('https://github.com/owner/repo/blob/main/file.ts')).toBe( - 'owner/repo' - ); - }); - }); - - describe('SSH URLs', () => { - it('extracts owner/repo from GitHub SSH URL', () => { - expect(extractRepoFromGitUrl('git@github.com:owner/repo.git')).toBe('owner/repo'); - }); - - it('extracts owner/repo from GitHub SSH URL without .git suffix', () => { - expect(extractRepoFromGitUrl('git@github.com:owner/repo')).toBe('owner/repo'); - }); - - it('extracts group/project from GitLab SSH URL', () => { - expect(extractRepoFromGitUrl('git@gitlab.com:group/project.git')).toBe('group/project'); - }); - - it('extracts nested group/subgroup/project from GitLab SSH URL', () => { - expect(extractRepoFromGitUrl('git@gitlab.com:group/subgroup/project.git')).toBe( - 'group/subgroup/project' - ); - }); - - it('extracts deeply nested GitLab project path from SSH URL', () => { - expect(extractRepoFromGitUrl('git@gitlab.com:a/b/c/d/project.git')).toBe('a/b/c/d/project'); - }); - }); - - describe('edge cases', () => { - it('returns undefined for null input', () => { - expect(extractRepoFromGitUrl(null)).toBeUndefined(); - }); - - it('returns undefined for undefined input', () => { - expect(extractRepoFromGitUrl(undefined)).toBeUndefined(); - }); - - it('returns undefined for empty string', () => { - expect(extractRepoFromGitUrl('')).toBeUndefined(); - }); - - it('returns undefined for invalid URL', () => { - expect(extractRepoFromGitUrl('not-a-url')).toBeUndefined(); - }); - - it('returns undefined for URL with only one path segment', () => { - expect(extractRepoFromGitUrl('https://github.com/owner')).toBeUndefined(); - }); - }); -}); - -describe('buildPrepareSessionRepoParams', () => { - it('uses gitlabProject when platform is gitlab', () => { - expect( - buildPrepareSessionRepoParams({ - repo: 'group/project', - platform: 'gitlab', - }) - ).toEqual({ gitlabProject: 'group/project' }); - }); - - it('uses githubRepo when platform is github', () => { - expect( - buildPrepareSessionRepoParams({ - repo: 'owner/repo', - platform: 'github', - }) - ).toEqual({ githubRepo: 'owner/repo' }); - }); - - it('returns null when repo is empty', () => { - expect(buildPrepareSessionRepoParams({ repo: ' ', platform: 'github' })).toBeNull(); - }); - - it('returns null when repo is null', () => { - expect(buildPrepareSessionRepoParams({ repo: null, platform: 'github' })).toBeNull(); - }); - - it('handles self-hosted GitLab via explicit platform (no gitlab in URL needed)', () => { - // This is the key fix: self-hosted GitLab instances with custom domains - // (e.g. git.company.com) are correctly handled via explicit platform - expect( - buildPrepareSessionRepoParams({ - repo: 'team/project', - platform: 'gitlab', - }) - ).toEqual({ gitlabProject: 'team/project' }); - }); - - it('handles nested GitLab group/subgroup via explicit platform', () => { - expect( - buildPrepareSessionRepoParams({ - repo: 'group/subgroup/project', - platform: 'gitlab', - }) - ).toEqual({ gitlabProject: 'group/subgroup/project' }); - }); -}); diff --git a/apps/web/src/components/cloud-agent/utils/git-utils.ts b/apps/web/src/components/cloud-agent/utils/git-utils.ts index c3d718d56a7..04092aea5ce 100644 --- a/apps/web/src/components/cloud-agent/utils/git-utils.ts +++ b/apps/web/src/components/cloud-agent/utils/git-utils.ts @@ -1,70 +1,2 @@ -/** - * Git URL Utilities - * - * Pure utility functions for parsing and extracting information from git URLs. - * This module has no dependencies on other cloud-agent modules to avoid circular imports. - */ - -import { PLATFORM } from '@/lib/integrations/core/constants'; - -/** - * Extract owner/repo or group/project from a git URL - * - * Supports formats: - * - https://github.com/owner/repo - * - https://github.com/owner/repo.git - * - git@github.com:owner/repo.git - * - git@github.com:owner/repo - * - https://gitlab.com/group/project.git - * - https://gitlab.com/group/subgroup/project.git (nested GitLab groups) - * - git@gitlab.com:group/project.git - * - git@gitlab.com:group/subgroup/project.git (nested GitLab groups) - * - * @param gitUrl - The git URL to parse - * @returns owner/repo or group/project format string, or undefined if parsing fails - */ -export function extractRepoFromGitUrl(gitUrl: string | null | undefined): string | undefined { - if (!gitUrl) return undefined; - - // SSH format: git@github.com:owner/repo.git or git@gitlab.com:group/subgroup/project.git - const sshMatch = gitUrl.match(/^git@[^:]+:(.+?)(?:\.git)?$/); - if (sshMatch) { - return sshMatch[1]; - } - - // HTTPS format: https://github.com/owner/repo.git or https://gitlab.com/group/subgroup/project.git - try { - const url = new URL(gitUrl); - const pathParts = url.pathname.split('/').filter(Boolean); - if (pathParts.length >= 2) { - // For clone URLs (ending in .git), use the full path (supports nested GitLab groups) - // For non-clone URLs (like GitHub tree URLs), only use first two segments - const fullPath = pathParts.join('/'); - if (fullPath.endsWith('.git')) { - return fullPath.replace(/\.git$/, ''); - } - // Non-clone URL: only take owner/repo (first two segments) - return `${pathParts[0]}/${pathParts[1]}`; - } - } catch { - // Not a valid URL - } - - return undefined; -} - -export type GitPlatform = 'github' | 'gitlab'; - -export function buildPrepareSessionRepoParams(options: { - repo?: string | null; - platform: GitPlatform; -}): { githubRepo?: string; gitlabProject?: string } | null { - const repo = options.repo?.trim(); - if (!repo) return null; - - if (options.platform === PLATFORM.GITLAB) { - return { gitlabProject: repo }; - } - - return { githubRepo: repo }; -} +export { extractRepoFromGitUrl } from '@/components/cloud-agent-next/utils/git-utils'; +export type { GitPlatform } from '@/components/cloud-agent-next/utils/git-utils';