modification in git-service oct 14
This commit is contained in:
parent
0d3f89da0f
commit
6698d11597
7
package-lock.json
generated
7
package-lock.json
generated
@ -3708,7 +3708,7 @@
|
||||
"version": "19.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
||||
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@ -3718,7 +3718,7 @@
|
||||
"version": "19.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
|
||||
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
@ -4896,7 +4896,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
@ -8393,7 +8393,6 @@
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
|
||||
422
src/app/vcs/repos/page.tsx
Normal file
422
src/app/vcs/repos/page.tsx
Normal 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;
|
||||
@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
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 { CustomTemplateForm } from "@/components/custom-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 { getAccessToken } from "@/components/apis/authApiClients"
|
||||
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 { ErrorBanner } from "@/components/ui/error-banner"
|
||||
|
||||
@ -126,7 +127,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
||||
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)
|
||||
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)) {
|
||||
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
|
||||
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(() => {
|
||||
connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
|
||||
connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((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)
|
||||
return
|
||||
@ -173,15 +177,18 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
||||
// Reset loading state before showing dialog
|
||||
setIsGeneratingAuth(false)
|
||||
|
||||
// Repository not accessible with current GitHub account - prompt to re-authenticate
|
||||
// Repository not accessible with current account - prompt to re-authenticate
|
||||
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) {
|
||||
console.log('🔐 Re-authenticating with GitHub for private repository access')
|
||||
connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => {
|
||||
console.log(`🔐 Re-authenticating with ${detectedProvider.toUpperCase()} for private repository access`)
|
||||
connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((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)
|
||||
@ -205,21 +212,10 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
||||
const handleOAuthAuth = async (provider: string) => {
|
||||
setAuthLoading(true)
|
||||
try {
|
||||
const providerConfig = gitProviders[provider as keyof typeof gitProviders]
|
||||
if (!providerConfig.oauthEndpoint) {
|
||||
throw new Error('OAuth not supported for this provider')
|
||||
}
|
||||
console.log(`🔐 [handleOAuthAuth] Starting OAuth for provider: ${provider}`)
|
||||
|
||||
// For GitHub, use the new OAuth helper
|
||||
if (provider === 'github') {
|
||||
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...`)
|
||||
// Use the new VCS API for all providers
|
||||
await connectProvider(provider)
|
||||
} catch (error) {
|
||||
console.error('OAuth error:', error)
|
||||
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',
|
||||
placeholder: 'https://github.com/org/repo.git',
|
||||
authMethods: ['token', 'ssh', 'oauth'],
|
||||
oauthEndpoint: '/api/auth/github',
|
||||
oauthEndpoint: '/api/vcs/github/auth/start',
|
||||
apiEndpoint: 'https://api.github.com'
|
||||
},
|
||||
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',
|
||||
placeholder: 'https://bitbucket.org/org/repo.git',
|
||||
authMethods: ['username_password', 'app_password', 'oauth'],
|
||||
oauthEndpoint: '/api/auth/bitbucket',
|
||||
oauthEndpoint: '/api/vcs/bitbucket/auth/start',
|
||||
apiEndpoint: 'https://api.bitbucket.org/2.0'
|
||||
},
|
||||
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',
|
||||
placeholder: 'https://gitlab.com/org/repo.git',
|
||||
authMethods: ['token', 'oauth', 'ssh'],
|
||||
oauthEndpoint: '/api/auth/gitlab',
|
||||
oauthEndpoint: '/api/vcs/gitlab/auth/start',
|
||||
apiEndpoint: 'https://gitlab.com/api/v4'
|
||||
},
|
||||
other: {
|
||||
@ -550,6 +546,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
||||
alert('Please enter a Git repository URL');
|
||||
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)
|
||||
try {
|
||||
await attachRepository({
|
||||
@ -568,15 +570,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
||||
// Return a mock template object to proceed to next step
|
||||
return {
|
||||
id: 'git-imported',
|
||||
title: `Imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}`,
|
||||
description: `Template imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}: ${gitUrl}`,
|
||||
title: `Imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}`,
|
||||
description: `Template imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}: ${gitUrl}`,
|
||||
type: 'custom',
|
||||
category: 'imported',
|
||||
is_custom: true,
|
||||
source: 'git',
|
||||
git_url: gitUrl.trim(),
|
||||
git_branch: gitBranch?.trim() || 'main',
|
||||
git_provider: gitProvider
|
||||
git_provider: detectedProvider
|
||||
}
|
||||
} catch (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 })
|
||||
|
||||
// If backend signals GitHub auth required, open the OAuth URL for this user
|
||||
if (status === 401 && (data?.requires_auth || data?.message?.includes('authentication'))) {
|
||||
const url: string = data?.auth_url
|
||||
if (!url) {
|
||||
// If backend signals auth required, redirect to provider OAuth
|
||||
if ((status === 401 || status === 200) && (data?.requires_auth || data?.message?.includes('authentication'))) {
|
||||
const authUrl: string = data?.auth_url
|
||||
if (!authUrl) {
|
||||
alert('Authentication URL is missing.');
|
||||
return
|
||||
}
|
||||
@ -599,13 +601,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
||||
try {
|
||||
sessionStorage.setItem('pending_git_attach', JSON.stringify({
|
||||
repository_url: gitUrl.trim(),
|
||||
branch_name: (gitBranch?.trim() || undefined)
|
||||
branch_name: (gitBranch?.trim() || undefined),
|
||||
provider: detectedProvider
|
||||
}))
|
||||
} catch {}
|
||||
|
||||
console.log('🔐 Redirecting to GitHub OAuth for repository attachment:', url)
|
||||
// Force same-tab redirect directly to GitHub consent screen
|
||||
window.location.replace(url)
|
||||
console.log(`🔐 Redirecting to ${detectedProvider.toUpperCase()} OAuth for repository attachment:`, authUrl)
|
||||
console.log(`🔐 Provider being passed to connectProvider:`, detectedProvider)
|
||||
// Use connectProvider function to handle OAuth flow properly
|
||||
await connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim())
|
||||
return
|
||||
}
|
||||
|
||||
@ -863,6 +867,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
||||
{/* Right-aligned quick navigation to user repos */}
|
||||
<div className="flex justify-end space-x-2">
|
||||
<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 className="space-y-4">
|
||||
|
||||
63
src/components/vcs/VcsConnectionButton.tsx
Normal file
63
src/components/vcs/VcsConnectionButton.tsx
Normal 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
239
src/lib/api/vcs.ts
Normal 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);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user