modification in git-service oct 14

This commit is contained in:
Pradeep 2025-10-15 08:03:29 +05:30
parent 0d3f89da0f
commit 6698d11597
5 changed files with 777 additions and 44 deletions

7
package-lock.json generated
View File

@ -3708,7 +3708,7 @@
"version": "19.1.10", "version": "19.1.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -3718,7 +3718,7 @@
"version": "19.1.7", "version": "19.1.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.0.0" "@types/react": "^19.0.0"
@ -4896,7 +4896,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": { "node_modules/d3-array": {
@ -8393,7 +8393,6 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-redux": { "node_modules/react-redux": {

422
src/app/vcs/repos/page.tsx Normal file
View File

@ -0,0 +1,422 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import {
Github,
Gitlab,
FolderOpen,
Search,
RefreshCw,
ExternalLink,
GitBranch,
Star,
Eye,
Code,
Calendar,
GitCompare,
Brain,
GitCommit,
Server
} from 'lucide-react';
import { authApiClient } from '@/components/apis/authApiClients';
import Link from 'next/link';
interface VcsRepoSummary {
id: string;
name: string;
full_name: string;
description?: string;
language?: string;
visibility: 'public' | 'private';
provider: 'github' | 'gitlab' | 'bitbucket' | 'gitea';
html_url: string;
clone_url: string;
default_branch: string;
stargazers_count?: number;
watchers_count?: number;
forks_count?: number;
updated_at: string;
created_at: string;
}
const VcsReposPage: React.FC = () => {
const [repositories, setRepositories] = useState<VcsRepoSummary[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [filter, setFilter] = useState<'all' | 'public' | 'private'>('all');
const [providerFilter, setProviderFilter] = useState<'all' | 'github' | 'gitlab' | 'bitbucket' | 'gitea'>('all');
const [aiAnalysisLoading, setAiAnalysisLoading] = useState<string | null>(null);
const [aiAnalysisError, setAiAnalysisError] = useState<string | null>(null);
// Handle OAuth callback for all providers
useEffect(() => {
const handleVcsCallback = async () => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
if (error) {
console.error('VCS OAuth error:', error);
alert(`OAuth error: ${error}`);
return;
}
if (code && state) {
console.log('🔐 [VCS OAuth] Callback received, processing...');
try {
// Check if we have a pending repository attachment
const pendingAttach = sessionStorage.getItem('pending_git_attach');
if (pendingAttach) {
const { repository_url, branch_name, provider } = JSON.parse(pendingAttach);
console.log(`🔐 [VCS OAuth] Resuming repository attachment:`, { repository_url, branch_name, provider });
// Clear the pending attach
sessionStorage.removeItem('pending_git_attach');
// Now attach the repository
try {
const response = await authApiClient.post(`/api/vcs/${provider}/attach-repository`, {
repository_url,
branch_name: branch_name || undefined,
});
alert(`${provider?.toUpperCase()} account connected and repository attached successfully!`);
// Clean up URL parameters
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
// Refresh the page to show the attached repository
window.location.reload();
} catch (attachError) {
console.error('Failed to attach repository after OAuth:', attachError);
alert(`${provider?.toUpperCase()} account connected, but failed to attach repository. Please try again.`);
}
} else {
console.log('🔐 [VCS OAuth] No pending repository attachment, just connecting account');
alert('Account connected successfully!');
// Clean up URL parameters
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
// Refresh repositories
loadRepositories();
}
} catch (error) {
console.error('Error handling VCS OAuth callback:', error);
alert('Error processing OAuth callback');
}
}
};
handleVcsCallback();
}, []);
// Load repositories from all providers
const loadRepositories = async () => {
try {
setIsLoading(true);
setError(null);
const allRepos: VcsRepoSummary[] = [];
// Try to load from each provider
const providers = ['github', 'gitlab', 'bitbucket', 'gitea'];
for (const provider of providers) {
try {
const response = await authApiClient.get(`/api/vcs/${provider}/repositories`);
const repos = response.data?.data || [];
// Add provider info to each repo
const reposWithProvider = repos.map((repo: any) => ({
...repo,
provider: provider
}));
allRepos.push(...reposWithProvider);
} catch (err) {
console.log(`No ${provider} repositories or not connected:`, err);
}
}
setRepositories(allRepos);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load repositories');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadRepositories();
}, []);
const handleRefresh = async () => {
await loadRepositories();
};
const handleAiAnalysis = async (repositoryId: string) => {
try {
setAiAnalysisLoading(repositoryId);
setAiAnalysisError(null);
const response = await authApiClient.get(`/api/ai-stream/repository/${repositoryId}/ai-stream`);
const data = response.data;
console.log('AI Analysis Result:', data);
alert('AI Analysis completed successfully!');
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'AI Analysis failed';
setAiAnalysisError(errorMessage);
console.error('AI Analysis Error:', err);
} finally {
setAiAnalysisLoading(null);
}
};
const getProviderIcon = (provider: string) => {
switch (provider) {
case 'github': return <Github className="h-4 w-4" />;
case 'gitlab': return <Gitlab className="h-4 w-4" />;
case 'bitbucket': return <GitCommit className="h-4 w-4" />;
case 'gitea': return <Server className="h-4 w-4" />;
default: return <GitCommit className="h-4 w-4" />;
}
};
const getProviderColor = (provider: string) => {
switch (provider) {
case 'github': return 'bg-gray-100 text-gray-800';
case 'gitlab': return 'bg-orange-100 text-orange-800';
case 'bitbucket': return 'bg-blue-100 text-blue-800';
case 'gitea': return 'bg-green-100 text-green-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const filteredRepositories = repositories.filter(repo => {
const matchesSearch = repo.full_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
repo.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
repo.language?.toLowerCase().includes(searchQuery.toLowerCase());
const matchesFilter = filter === 'all' ||
(filter === 'public' && repo.visibility === 'public') ||
(filter === 'private' && repo.visibility === 'private');
const matchesProvider = providerFilter === 'all' || repo.provider === providerFilter;
return matchesSearch && matchesFilter && matchesProvider;
});
return (
<div className="container mx-auto p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2">My Repositories</h1>
<p className="text-gray-600">Manage and analyze your repositories from all connected providers</p>
</div>
{/* Search and Filters */}
<div className="mb-6 space-y-4">
<div className="flex gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Search repositories..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Button onClick={handleRefresh} disabled={isLoading} variant="outline">
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
<div className="flex gap-4">
<div className="flex gap-2">
<Button
variant={filter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter('all')}
>
All
</Button>
<Button
variant={filter === 'public' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter('public')}
>
Public
</Button>
<Button
variant={filter === 'private' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter('private')}
>
Private
</Button>
</div>
<div className="flex gap-2">
<Button
variant={providerFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setProviderFilter('all')}
>
All Providers
</Button>
<Button
variant={providerFilter === 'github' ? 'default' : 'outline'}
size="sm"
onClick={() => setProviderFilter('github')}
>
<Github className="h-4 w-4 mr-1" />
GitHub
</Button>
<Button
variant={providerFilter === 'gitlab' ? 'default' : 'outline'}
size="sm"
onClick={() => setProviderFilter('gitlab')}
>
<Gitlab className="h-4 w-4 mr-1" />
GitLab
</Button>
<Button
variant={providerFilter === 'bitbucket' ? 'default' : 'outline'}
size="sm"
onClick={() => setProviderFilter('bitbucket')}
>
<GitCommit className="h-4 w-4 mr-1" />
Bitbucket
</Button>
<Button
variant={providerFilter === 'gitea' ? 'default' : 'outline'}
size="sm"
onClick={() => setProviderFilter('gitea')}
>
<Server className="h-4 w-4 mr-1" />
Gitea
</Button>
</div>
</div>
</div>
{/* Error State */}
{error && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-800">{error}</p>
</div>
)}
{/* Loading State */}
{isLoading && (
<div className="flex justify-center items-center py-8">
<RefreshCw className="h-8 w-8 animate-spin text-gray-400" />
<span className="ml-2 text-gray-600">Loading repositories...</span>
</div>
)}
{/* Repositories Grid */}
{!isLoading && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredRepositories.map((repo) => (
<Card key={repo.id} className="hover:shadow-lg transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-2">
{getProviderIcon(repo.provider)}
<CardTitle className="text-lg truncate">{repo.name}</CardTitle>
</div>
<Badge variant={repo.visibility === 'public' ? 'default' : 'secondary'}>
{repo.visibility}
</Badge>
</div>
<div className="flex items-center space-x-2">
<Badge className={getProviderColor(repo.provider)}>
{repo.provider.toUpperCase()}
</Badge>
</div>
{repo.description && (
<p className="text-sm text-gray-600 line-clamp-2">{repo.description}</p>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="flex items-center space-x-4 text-sm text-gray-500 mb-4">
{repo.language && (
<div className="flex items-center space-x-1">
<Code className="h-4 w-4" />
<span>{repo.language}</span>
</div>
)}
<div className="flex items-center space-x-1">
<GitBranch className="h-4 w-4" />
<span>{repo.default_branch}</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex space-x-2">
<Button
size="sm"
variant="outline"
onClick={() => window.open(repo.html_url, '_blank')}
>
<ExternalLink className="h-4 w-4 mr-1" />
View
</Button>
<Button
size="sm"
onClick={() => handleAiAnalysis(repo.id)}
disabled={aiAnalysisLoading === repo.id}
>
<Brain className="h-4 w-4 mr-1" />
{aiAnalysisLoading === repo.id ? 'Analyzing...' : 'AI Analysis'}
</Button>
</div>
</div>
{aiAnalysisError && (
<p className="text-red-600 text-sm mt-2">{aiAnalysisError}</p>
)}
</CardContent>
</Card>
))}
</div>
)}
{/* Empty State */}
{!isLoading && filteredRepositories.length === 0 && (
<div className="text-center py-12">
<FolderOpen className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No repositories found</h3>
<p className="text-gray-600 mb-4">
{searchQuery || filter !== 'all' || providerFilter !== 'all'
? 'Try adjusting your search or filters'
: 'Connect your accounts to see your repositories'
}
</p>
<Link href="/dashboard">
<Button>Go to Dashboard</Button>
</Link>
</div>
)}
</div>
);
};
export default VcsReposPage;

View File

@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette, GitBranch } from "lucide-react" import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette, GitBranch, FolderGit2 } from "lucide-react"
import { useTemplates } from "@/hooks/useTemplates" import { useTemplates } from "@/hooks/useTemplates"
import { CustomTemplateForm } from "@/components/custom-template-form" import { CustomTemplateForm } from "@/components/custom-template-form"
import { EditTemplateForm } from "@/components/edit-template-form" import { EditTemplateForm } from "@/components/edit-template-form"
@ -23,7 +23,8 @@ import PromptSidePanel from "@/components/prompt-side-panel"
import { DualCanvasEditor } from "@/components/dual-canvas-editor" import { DualCanvasEditor } from "@/components/dual-canvas-editor"
import { getAccessToken } from "@/components/apis/authApiClients" import { getAccessToken } from "@/components/apis/authApiClients"
import TechStackSummary from "@/components/tech-stack-summary" import TechStackSummary from "@/components/tech-stack-summary"
import { attachRepository, getGitHubAuthStatus, AttachRepositoryResponse, connectGitHubWithRepo, initiateGitHubOAuth } from "@/lib/api/github" import { attachRepository, detectProvider, connectProvider, AttachRepositoryResponse } from "@/lib/api/vcs"
import { getGitHubAuthStatus } from "@/lib/api/github"
import ViewUserReposButton from "@/components/github/ViewUserReposButton" import ViewUserReposButton from "@/components/github/ViewUserReposButton"
import { ErrorBanner } from "@/components/ui/error-banner" import { ErrorBanner } from "@/components/ui/error-banner"
@ -126,7 +127,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
throw new Error('Repository attachment failed') throw new Error('Repository attachment failed')
} }
const isPrivate = result?.data?.is_public === false || result?.data?.requires_auth === true const isPrivate = result?.data?.is_public === false
console.log('✅ Repository attached successfully:', result) console.log('✅ Repository attached successfully:', result)
const repoType = isPrivate ? 'private' : 'public' const repoType = isPrivate ? 'private' : 'public'
@ -155,15 +156,18 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
}) })
if (status === 401 && (data?.requires_auth || data?.auth_url || data?.service_auth_url)) { if (status === 401 && (data?.requires_auth || data?.auth_url || data?.service_auth_url)) {
console.log('🔐 Private repository detected - initiating GitHub OAuth with repository context') console.log('🔐 Private repository detected - initiating OAuth with repository context')
// Reset loading state before redirect // Reset loading state before redirect
setIsGeneratingAuth(false) setIsGeneratingAuth(false)
// Use the new OAuth helper that will auto-attach the repo after authentication // Detect provider from URL
const detectedProvider = detectProvider(gitUrl.trim());
// Use the universal OAuth helper that will auto-attach the repo after authentication
setTimeout(() => { setTimeout(() => {
connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => { connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
console.error('OAuth initiation failed:', oauthError) console.error('OAuth initiation failed:', oauthError)
alert('Failed to initiate GitHub authentication. Please try again.') alert(`Failed to initiate ${detectedProvider.toUpperCase()} authentication. Please try again.`)
}) })
}, 100) }, 100)
return return
@ -173,15 +177,18 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
// Reset loading state before showing dialog // Reset loading state before showing dialog
setIsGeneratingAuth(false) setIsGeneratingAuth(false)
// Repository not accessible with current GitHub account - prompt to re-authenticate // Repository not accessible with current account - prompt to re-authenticate
setTimeout(() => { setTimeout(() => {
const confirmReauth = confirm('Repository not accessible with your current GitHub account.\n\nThis could mean:\n- The repository belongs to a different GitHub account\n- Your token expired or lacks permissions\n\nWould you like to re-authenticate with GitHub?') // Detect provider from URL
const detectedProvider = detectProvider(gitUrl.trim());
const confirmReauth = confirm(`Repository not accessible with your current ${detectedProvider.toUpperCase()} account.\n\nThis could mean:\n- The repository belongs to a different ${detectedProvider.toUpperCase()} account\n- Your token expired or lacks permissions\n\nWould you like to re-authenticate with ${detectedProvider.toUpperCase()}?`)
if (confirmReauth) { if (confirmReauth) {
console.log('🔐 Re-authenticating with GitHub for private repository access') console.log(`🔐 Re-authenticating with ${detectedProvider.toUpperCase()} for private repository access`)
connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => { connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
console.error('OAuth initiation failed:', oauthError) console.error('OAuth initiation failed:', oauthError)
alert('Failed to initiate GitHub authentication. Please try again.') alert(`Failed to initiate ${detectedProvider.toUpperCase()} authentication. Please try again.`)
}) })
} }
}, 100) }, 100)
@ -205,21 +212,10 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
const handleOAuthAuth = async (provider: string) => { const handleOAuthAuth = async (provider: string) => {
setAuthLoading(true) setAuthLoading(true)
try { try {
const providerConfig = gitProviders[provider as keyof typeof gitProviders] console.log(`🔐 [handleOAuthAuth] Starting OAuth for provider: ${provider}`)
if (!providerConfig.oauthEndpoint) {
throw new Error('OAuth not supported for this provider')
}
// For GitHub, use the new OAuth helper // Use the new VCS API for all providers
if (provider === 'github') { await connectProvider(provider)
console.log('Initiating GitHub OAuth flow...')
initiateGitHubOAuth()
return
}
// For other providers, use the old method
window.open(providerConfig.oauthEndpoint, '_blank', 'width=600,height=700')
alert(`Redirecting to ${providerConfig.name} OAuth...`)
} catch (error) { } catch (error) {
console.error('OAuth error:', error) console.error('OAuth error:', error)
alert('OAuth authentication failed') alert('OAuth authentication failed')
@ -294,7 +290,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
icon: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z', icon: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z',
placeholder: 'https://github.com/org/repo.git', placeholder: 'https://github.com/org/repo.git',
authMethods: ['token', 'ssh', 'oauth'], authMethods: ['token', 'ssh', 'oauth'],
oauthEndpoint: '/api/auth/github', oauthEndpoint: '/api/vcs/github/auth/start',
apiEndpoint: 'https://api.github.com' apiEndpoint: 'https://api.github.com'
}, },
bitbucket: { bitbucket: {
@ -302,7 +298,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
icon: 'M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891L.778 1.213zM14.518 18.197H9.482l-1.4-8.893h7.436l-1.4 8.893z', icon: 'M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891L.778 1.213zM14.518 18.197H9.482l-1.4-8.893h7.436l-1.4 8.893z',
placeholder: 'https://bitbucket.org/org/repo.git', placeholder: 'https://bitbucket.org/org/repo.git',
authMethods: ['username_password', 'app_password', 'oauth'], authMethods: ['username_password', 'app_password', 'oauth'],
oauthEndpoint: '/api/auth/bitbucket', oauthEndpoint: '/api/vcs/bitbucket/auth/start',
apiEndpoint: 'https://api.bitbucket.org/2.0' apiEndpoint: 'https://api.bitbucket.org/2.0'
}, },
gitlab: { gitlab: {
@ -310,7 +306,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
icon: 'M23.6004 9.5927l-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.874.874 0 0 0-.9997.0539.874.874 0 0 0-.29.4399l-2.5465 7.7838H7.2162l-2.5465-7.7838a.857.857 0 0 0-.29-.4412.874.874 0 0 0-.9997-.0537.858.858 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.065 6.065 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.008 1.008 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7466.0125-.01a6.068 6.068 0 0 0 2.0094-7.003z', icon: 'M23.6004 9.5927l-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.874.874 0 0 0-.9997.0539.874.874 0 0 0-.29.4399l-2.5465 7.7838H7.2162l-2.5465-7.7838a.857.857 0 0 0-.29-.4412.874.874 0 0 0-.9997-.0537.858.858 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.065 6.065 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.008 1.008 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7466.0125-.01a6.068 6.068 0 0 0 2.0094-7.003z',
placeholder: 'https://gitlab.com/org/repo.git', placeholder: 'https://gitlab.com/org/repo.git',
authMethods: ['token', 'oauth', 'ssh'], authMethods: ['token', 'oauth', 'ssh'],
oauthEndpoint: '/api/auth/gitlab', oauthEndpoint: '/api/vcs/gitlab/auth/start',
apiEndpoint: 'https://gitlab.com/api/v4' apiEndpoint: 'https://gitlab.com/api/v4'
}, },
other: { other: {
@ -550,6 +546,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
alert('Please enter a Git repository URL'); alert('Please enter a Git repository URL');
return; return;
} }
// Detect provider from URL
const detectedProvider = detectProvider(gitUrl.trim());
console.log('🔍 [handleCreateFromGit] Detected provider:', detectedProvider);
console.log('🔍 [handleCreateFromGit] Git URL:', gitUrl.trim());
// Attach the repository via backend (skip template creation) // Attach the repository via backend (skip template creation)
try { try {
await attachRepository({ await attachRepository({
@ -568,15 +570,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
// Return a mock template object to proceed to next step // Return a mock template object to proceed to next step
return { return {
id: 'git-imported', id: 'git-imported',
title: `Imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}`, title: `Imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}`,
description: `Template imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}: ${gitUrl}`, description: `Template imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}: ${gitUrl}`,
type: 'custom', type: 'custom',
category: 'imported', category: 'imported',
is_custom: true, is_custom: true,
source: 'git', source: 'git',
git_url: gitUrl.trim(), git_url: gitUrl.trim(),
git_branch: gitBranch?.trim() || 'main', git_branch: gitBranch?.trim() || 'main',
git_provider: gitProvider git_provider: detectedProvider
} }
} catch (attachErr) { } catch (attachErr) {
console.error('[TemplateSelectionStep] attachRepository failed:', attachErr) console.error('[TemplateSelectionStep] attachRepository failed:', attachErr)
@ -587,10 +589,10 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
console.log('🔍 HandleCreateFromGit error response:', { status, data }) console.log('🔍 HandleCreateFromGit error response:', { status, data })
// If backend signals GitHub auth required, open the OAuth URL for this user // If backend signals auth required, redirect to provider OAuth
if (status === 401 && (data?.requires_auth || data?.message?.includes('authentication'))) { if ((status === 401 || status === 200) && (data?.requires_auth || data?.message?.includes('authentication'))) {
const url: string = data?.auth_url const authUrl: string = data?.auth_url
if (!url) { if (!authUrl) {
alert('Authentication URL is missing.'); alert('Authentication URL is missing.');
return return
} }
@ -599,13 +601,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
try { try {
sessionStorage.setItem('pending_git_attach', JSON.stringify({ sessionStorage.setItem('pending_git_attach', JSON.stringify({
repository_url: gitUrl.trim(), repository_url: gitUrl.trim(),
branch_name: (gitBranch?.trim() || undefined) branch_name: (gitBranch?.trim() || undefined),
provider: detectedProvider
})) }))
} catch {} } catch {}
console.log('🔐 Redirecting to GitHub OAuth for repository attachment:', url) console.log(`🔐 Redirecting to ${detectedProvider.toUpperCase()} OAuth for repository attachment:`, authUrl)
// Force same-tab redirect directly to GitHub consent screen console.log(`🔐 Provider being passed to connectProvider:`, detectedProvider)
window.location.replace(url) // Use connectProvider function to handle OAuth flow properly
await connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim())
return return
} }
@ -863,6 +867,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
{/* Right-aligned quick navigation to user repos */} {/* Right-aligned quick navigation to user repos */}
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
<ViewUserReposButton className="bg-orange-500 hover:bg-orange-400 text-black" label="My GitHub Repos" /> <ViewUserReposButton className="bg-orange-500 hover:bg-orange-400 text-black" label="My GitHub Repos" />
<Link href="/vcs/repos">
<Button className="bg-blue-500 hover:bg-blue-400 text-white">
<FolderGit2 className="mr-2 h-5 w-5" />
All My Repos
</Button>
</Link>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">

View File

@ -0,0 +1,63 @@
"use client"
import { Button } from "@/components/ui/button"
import { Github, Gitlab, GitCommit, Server } from "lucide-react"
import { connectProvider } from "@/lib/api/vcs"
interface Props {
provider: 'github' | 'gitlab' | 'bitbucket' | 'gitea'
className?: string
size?: "default" | "sm" | "lg" | "icon"
label?: string
repoUrl?: string
branch?: string
}
export default function VcsConnectionButton({
provider,
className,
size = "default",
label,
repoUrl,
branch
}: Props) {
const getProviderIcon = () => {
switch (provider) {
case 'github': return <Github className="mr-2 h-5 w-5" />;
case 'gitlab': return <Gitlab className="mr-2 h-5 w-5" />;
case 'bitbucket': return <GitCommit className="mr-2 h-5 w-5" />;
case 'gitea': return <Server className="mr-2 h-5 w-5" />;
default: return <GitCommit className="mr-2 h-5 w-5" />;
}
};
const getProviderColor = () => {
switch (provider) {
case 'github': return 'bg-gray-600 hover:bg-gray-700';
case 'gitlab': return 'bg-orange-600 hover:bg-orange-700';
case 'bitbucket': return 'bg-blue-600 hover:bg-blue-700';
case 'gitea': return 'bg-green-600 hover:bg-green-700';
default: return 'bg-gray-600 hover:bg-gray-700';
}
};
const handleConnect = async () => {
try {
await connectProvider(provider, repoUrl, branch);
} catch (error) {
console.error(`Failed to connect ${provider}:`, error);
alert(`Failed to connect ${provider.toUpperCase()} account. Please try again.`);
}
};
return (
<Button
className={`${getProviderColor()} text-white ${className}`}
size={size}
onClick={handleConnect}
>
{getProviderIcon()}
{label || `Connect ${provider.charAt(0).toUpperCase() + provider.slice(1)}`}
</Button>
)
}

239
src/lib/api/vcs.ts Normal file
View File

@ -0,0 +1,239 @@
import { authApiClient } from '@/components/apis/authApiClients'
export interface AttachRepositoryPayload {
repository_url: string
branch_name?: string
user_id?: string
template_id?: string
}
export interface AttachRepositoryResponse {
success: boolean
message: string
data?: {
repository_id: string
repository_name: string
owner_name: string
branch_name: string
is_public: boolean
sync_status: string
webhook_result?: {
created: boolean
hook_id: string | number
}
storage_info?: {
total_files: number
total_directories: number
total_size_bytes: number
}
}
requires_auth?: boolean
auth_url?: string
auth_error?: boolean
}
// Provider detection function
export function detectProvider(repoUrl: string): string {
if (repoUrl.includes('github.com')) return 'github';
if (repoUrl.includes('gitlab.com') || repoUrl.includes('gitlab')) return 'gitlab';
if (repoUrl.includes('bitbucket.org')) return 'bitbucket';
if (repoUrl.includes('gitea')) return 'gitea';
throw new Error('Unsupported repository provider');
}
// Universal repository attachment function
export async function attachRepository(payload: AttachRepositoryPayload, retries = 3): Promise<AttachRepositoryResponse> {
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null
// Detect provider from URL
const provider = detectProvider(payload.repository_url);
console.log('🔍 [attachRepository] Detected provider:', provider);
const url = userId
? `/api/vcs/${provider}/attach-repository?user_id=${encodeURIComponent(userId)}`
: `/api/vcs/${provider}/attach-repository`;
// Retry logic for connection issues
for (let i = 0; i < retries; i++) {
try {
const response = await authApiClient.post(url, {
...payload,
user_id: userId
}, {
headers: { 'Content-Type': 'application/json' },
timeout: 60000 // 60 seconds for repository operations
});
console.log(`📡 [attachRepository] ${provider.toUpperCase()} response:`, response.data);
// Normalize response
let parsed: any = response.data;
if (typeof parsed === 'string') {
try {
parsed = JSON.parse(parsed);
} catch (e) {
console.warn('📡 [attachRepository] Failed to parse string response');
}
}
const normalized: AttachRepositoryResponse = {
...(parsed || {}),
success: (parsed?.success === true || parsed?.success === 'true')
};
return normalized;
} catch (error: any) {
// If it's the last retry or not a connection error, throw immediately
if (i === retries - 1 || (error.code !== 'ECONNREFUSED' && error.code !== 'ECONNRESET')) {
throw error;
}
// Wait before retrying (exponential backoff)
const waitTime = Math.min(1000 * Math.pow(2, i), 5000);
console.log(`📡 [attachRepository] Retry ${i + 1}/${retries} in ${waitTime}ms...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
throw new Error('Max retries exceeded');
}
// Universal OAuth connection function
export async function connectProvider(provider: string, repoUrl?: string, branch?: string): Promise<void> {
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null;
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null;
if (!userId) {
alert('Please sign in first to connect your account');
return;
}
// Build state with repository context if provided
const stateBase = Math.random().toString(36).substring(7);
let state = stateBase;
if (repoUrl) {
const encodedRepoUrl = encodeURIComponent(repoUrl);
const encodedBranch = encodeURIComponent(branch || 'main');
state = `${stateBase}|uid=${userId}|repo=${encodedRepoUrl}|branch=${encodedBranch}`;
// Store in sessionStorage for recovery
try {
sessionStorage.setItem('pending_git_attach', JSON.stringify({
repository_url: repoUrl,
branch_name: branch || 'main',
provider: provider
}));
} catch (e) {
console.warn('Failed to store pending attach in sessionStorage:', e);
}
}
console.log(`🔗 [connectProvider] Connecting ${provider.toUpperCase()} account for user:`, userId);
console.log(`🔗 [connectProvider] API URL: /api/vcs/${provider}/auth/start?user_id=${encodeURIComponent(userId)}&state=${encodeURIComponent(state)}`);
try {
const response = await authApiClient.get(
`/api/vcs/${provider}/auth/start?user_id=${encodeURIComponent(userId)}&state=${encodeURIComponent(state)}`
);
console.log(`🔗 [connectProvider] Backend response:`, response.data);
const authUrl = response.data?.auth_url;
if (authUrl) {
console.log(`🔗 [connectProvider] Redirecting to ${provider.toUpperCase()} OAuth:`, authUrl);
console.log(`🔗 [connectProvider] Auth URL domain:`, new URL(authUrl).hostname);
window.location.replace(authUrl);
} else {
console.error('No auth URL in response:', response.data);
throw new Error('No auth URL received from backend');
}
} catch (error) {
console.error(`Failed to get ${provider.toUpperCase()} OAuth URL:`, error);
throw error;
}
}
// Provider-specific OAuth functions (for backward compatibility)
export async function connectGitHub(repoUrl?: string, branch?: string): Promise<void> {
return connectProvider('github', repoUrl, branch);
}
export async function connectGitLab(repoUrl?: string, branch?: string): Promise<void> {
return connectProvider('gitlab', repoUrl, branch);
}
export async function connectBitbucket(repoUrl?: string, branch?: string): Promise<void> {
return connectProvider('bitbucket', repoUrl, branch);
}
export async function connectGitea(repoUrl?: string, branch?: string): Promise<void> {
return connectProvider('gitea', repoUrl, branch);
}
// Repository structure and file content (works for all providers)
export interface RepoStructureEntry {
name: string
type: 'file' | 'directory'
size?: number
path: string
}
export interface RepoStructureResponse {
repository_id: string
directory_path: string
structure: RepoStructureEntry[]
}
export async function getRepositoryStructure(repositoryId: string, path: string = ''): Promise<RepoStructureResponse> {
// Try to detect provider from repository ID or use a generic endpoint
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/structure${path ? `?path=${encodeURIComponent(path)}` : ''}`;
const res = await authApiClient.get(url);
return res.data?.data as RepoStructureResponse;
}
export interface FileContentResponse {
file_info: {
id: string
filename: string
file_extension?: string
relative_path: string
file_size_bytes?: number
mime_type?: string
is_binary?: boolean
language_detected?: string
line_count?: number
char_count?: number
}
content: string | null
preview?: string | null
}
export async function getRepositoryFileContent(repositoryId: string, filePath: string): Promise<FileContentResponse> {
const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/file-content?file_path=${encodeURIComponent(filePath)}`;
const res = await authApiClient.get(url);
return res.data?.data as FileContentResponse;
}
// AI Streaming (works for all providers)
export async function startAIStreaming(repositoryId: string, options: {
fileTypes?: string[]
maxSize?: number
includeBinary?: boolean
chunkSize?: number
directoryFilter?: string
excludePatterns?: string[]
} = {}): Promise<EventSource> {
const params = new URLSearchParams();
if (options.fileTypes) params.append('file_types', options.fileTypes.join(','));
if (options.maxSize) params.append('max_size', options.maxSize.toString());
if (options.includeBinary) params.append('include_binary', 'true');
if (options.chunkSize) params.append('chunk_size', options.chunkSize.toString());
if (options.directoryFilter) params.append('directory_filter', options.directoryFilter);
if (options.excludePatterns) params.append('exclude_patterns', options.excludePatterns.join(','));
const url = `/api/ai-stream/repository/${encodeURIComponent(repositoryId)}/ai-stream?${params.toString()}`;
return new EventSource(url);
}