git integration backend changes

This commit is contained in:
Chandini 2025-09-29 10:59:19 +05:30
parent 7eb2ab1dc2
commit 84736d86a8
6 changed files with 421 additions and 123 deletions

View File

@ -19,17 +19,6 @@ CREATE TABLE IF NOT EXISTS github_repositories (
updated_at TIMESTAMP DEFAULT NOW()
);
-- Create table for feature-codebase mappings
CREATE TABLE IF NOT EXISTS feature_codebase_mappings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
template_id UUID REFERENCES templates(id) ON DELETE CASCADE,
feature_id UUID REFERENCES template_features(id) ON DELETE CASCADE,
repository_id UUID REFERENCES github_repositories(id) ON DELETE CASCADE,
file_paths TEXT[],
code_snippets JSONB,
implementation_notes TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_github_repos_template_id ON github_repositories(template_id);

View File

@ -1,35 +0,0 @@
-- Create table if it does not exist (compatible with existing schemas)
CREATE TABLE IF NOT EXISTS webhook_events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
event_type VARCHAR(100) NOT NULL,
processing_status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW()
);
-- Bring table to desired schema (idempotent)
ALTER TABLE webhook_events ADD COLUMN IF NOT EXISTS action VARCHAR(100);
ALTER TABLE webhook_events ADD COLUMN IF NOT EXISTS repository_full_name VARCHAR(255);
ALTER TABLE webhook_events ADD COLUMN IF NOT EXISTS delivery_id VARCHAR(100);
ALTER TABLE webhook_events ADD COLUMN IF NOT EXISTS metadata JSONB;
ALTER TABLE webhook_events ADD COLUMN IF NOT EXISTS received_at TIMESTAMP DEFAULT NOW();
ALTER TABLE webhook_events ADD COLUMN IF NOT EXISTS processed_at TIMESTAMP;
ALTER TABLE webhook_events ADD COLUMN IF NOT EXISTS error_message TEXT;
ALTER TABLE webhook_events ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW();
-- Create indexes (safe if columns now exist)
CREATE INDEX IF NOT EXISTS idx_webhook_events_event_type ON webhook_events(event_type);
CREATE INDEX IF NOT EXISTS idx_webhook_events_repository ON webhook_events(repository_full_name);
CREATE INDEX IF NOT EXISTS idx_webhook_events_received_at ON webhook_events(received_at);
CREATE INDEX IF NOT EXISTS idx_webhook_events_delivery_id ON webhook_events(delivery_id);
CREATE INDEX IF NOT EXISTS idx_webhook_events_status ON webhook_events(processing_status);
-- Add trigger to update timestamp
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger WHERE tgname = 'update_webhook_events_updated_at'
) THEN
CREATE TRIGGER update_webhook_events_updated_at BEFORE UPDATE ON webhook_events
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
END IF;
END $$;

View File

@ -29,41 +29,204 @@ router.post('/attach-repository', async (req, res) => {
// Parse GitHub URL
const { owner, repo, branch } = githubService.parseGitHubUrl(repository_url);
// Check repository access with user-specific tokens
const accessCheck = await githubService.checkRepositoryAccessWithUser(owner, repo, userId);
// First, try to check if this is a public repository without authentication
let isPublicRepo = false;
let repositoryData = null;
if (!accessCheck.hasAccess) {
if (accessCheck.requiresAuth || accessCheck.authError) {
// Generate an auth URL that encodes the current user and returns absolute via gateway
const state = Math.random().toString(36).substring(7);
try {
// Try to access the repository without authentication first (for public repos)
const unauthenticatedOctokit = new (require('@octokit/rest')).Octokit({
userAgent: 'CodeNuk-GitIntegration/1.0.0',
});
const { data: repoInfo } = await unauthenticatedOctokit.repos.get({ owner, repo });
isPublicRepo = !repoInfo.private;
repositoryData = {
full_name: repoInfo.full_name,
description: repoInfo.description,
language: repoInfo.language,
visibility: repoInfo.private ? 'private' : 'public',
stargazers_count: repoInfo.stargazers_count,
forks_count: repoInfo.forks_count,
default_branch: repoInfo.default_branch,
size: repoInfo.size,
updated_at: repoInfo.updated_at
};
console.log(`✅ Repository ${owner}/${repo} is ${isPublicRepo ? 'public' : 'private'}`);
} catch (error) {
// IMPORTANT: GitHub returns 404 for private repos when unauthenticated.
// Do NOT immediately return 404 here; instead continue to check auth and treat as potentially private.
if (error.status && error.status !== 404) {
// For non-404 errors (e.g., rate-limit, network), surface a meaningful message
console.warn(`Unauthenticated access failed with status ${error.status}: ${error.message}`);
}
// If we can't access it without auth (including 404), it's likely private - check if user has auth
console.log(`❌ Cannot access ${owner}/${repo} without authentication (status=${error.status || 'unknown'}), checking user auth...`);
// Check if user has GitHub authentication
let hasAuth = false;
try {
const authStatus = await oauthService.getAuthStatus();
hasAuth = authStatus.connected;
console.log(`🔐 User authentication status: ${hasAuth ? 'Connected' : 'Not connected'}`);
} catch (authError) {
console.log(`❌ Error checking auth status: ${authError.message}`);
hasAuth = false;
}
// If user is not authenticated, first optimistically try to attach as PUBLIC via git clone.
// If cloning fails (e.g., due to private permissions), then prompt OAuth.
if (!hasAuth) {
try {
// Minimal metadata assuming public repo
repositoryData = {
full_name: `${owner}/${repo}`,
description: null,
language: null,
visibility: 'public',
stargazers_count: 0,
forks_count: 0,
default_branch: branch || branch_name || 'main',
size: 0,
updated_at: new Date().toISOString()
};
isPublicRepo = true;
// Determine branch fallback
let actualBranchLocal = repositoryData.default_branch || 'main';
if (branch_name) actualBranchLocal = branch_name;
// Store DB record before syncing (sync_status=syncing)
const insertQueryPublic = `
INSERT INTO github_repositories (
repository_url, repository_name, owner_name,
branch_name, is_public, metadata, codebase_analysis, sync_status,
requires_auth, user_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *
`;
const insertValuesPublic = [
repository_url,
repo,
owner,
actualBranchLocal,
true,
JSON.stringify(repositoryData),
JSON.stringify({ branch: actualBranchLocal, total_files: 0, total_size: 0 }),
'syncing',
false,
userId || null
];
const insertResPublic = await database.query(insertQueryPublic, insertValuesPublic);
const repositoryRecordPublic = insertResPublic.rows[0];
// Try to sync via git without auth (attempt preferred branch, then alternate if needed)
let downloadResultPublic = await githubService.syncRepositoryWithFallback(
owner, repo, actualBranchLocal, repositoryRecordPublic.id, true
);
if (!downloadResultPublic.success) {
const altBranch = actualBranchLocal === 'main' ? 'master' : 'main';
console.warn(`First public sync attempt failed on '${actualBranchLocal}', retrying with alternate branch '${altBranch}'...`);
downloadResultPublic = await githubService.syncRepositoryWithFallback(
owner, repo, altBranch, repositoryRecordPublic.id, true
);
if (downloadResultPublic.success) {
// Persist branch switch
await database.query('UPDATE github_repositories SET branch_name = $1, updated_at = NOW() WHERE id = $2', [altBranch, repositoryRecordPublic.id]);
actualBranchLocal = altBranch;
}
}
const finalSyncStatusPublic = downloadResultPublic.success ? 'synced' : 'error';
await database.query(
'UPDATE github_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2',
[finalSyncStatusPublic, repositoryRecordPublic.id]
);
if (downloadResultPublic.success) {
const storageInfoPublic = await githubService.getRepositoryStorage(repositoryRecordPublic.id);
return res.status(201).json({
success: true,
message: 'Repository attached and synced successfully (public, no auth)',
data: {
repository_id: repositoryRecordPublic.id,
repository_name: repositoryRecordPublic.repository_name,
owner_name: repositoryRecordPublic.owner_name,
branch_name: repositoryRecordPublic.branch_name,
is_public: true,
requires_auth: false,
sync_status: finalSyncStatusPublic,
metadata: repositoryData,
codebase_analysis: { branch: actualBranchLocal },
storage_info: storageInfoPublic,
download_result: downloadResultPublic
}
});
}
// If we reach here, public clone failed: likely private → prompt OAuth now
console.warn('Public clone attempt failed; switching to OAuth requirement');
} catch (probeErr) {
console.warn('Optimistic public attach failed:', probeErr.message);
}
// Generate an auth URL that encodes the current user AND repo context so callback can auto-attach
const stateBase = Math.random().toString(36).substring(7);
const userIdForAuth = userId || null;
const encodedRepoUrl = encodeURIComponent(repository_url);
const encodedBranchName = encodeURIComponent(branch_name || '');
const state = `${stateBase}|uid=${userIdForAuth || ''}|repo=${encodedRepoUrl}|branch=${encodedBranchName}`;
const rawAuthUrl = oauthService.getAuthUrl(state, userIdForAuth);
// Prefer returning a gateway URL so frontend doesn't need to know service ports
const gatewayBase = process.env.API_GATEWAY_PUBLIC_URL || 'http://localhost:8000';
const serviceRelative = '/api/github/auth/github';
// redirect=1 makes the endpoint issue a 302 directly to GitHub so UI doesn't land on JSON
const serviceAuthUrl = `${gatewayBase}${serviceRelative}?redirect=1&state=${encodeURIComponent(state)}${userIdForAuth ? `&user_id=${encodeURIComponent(userIdForAuth)}` : ''}`;
return res.status(401).json({
success: false,
message: accessCheck.error || 'GitHub authentication required for this repository',
message: 'GitHub authentication required or repository is private',
requires_auth: true,
// Return both, frontend can pick the gateway URL
auth_url: serviceAuthUrl,
service_auth_url: rawAuthUrl,
auth_error: accessCheck.authError || false
auth_error: false
});
}
return res.status(404).json({
// User is authenticated, try to access the repository with auth
try {
const octokit = await githubService.getAuthenticatedOctokit();
const { data: repoInfo } = await octokit.repos.get({ owner, repo });
isPublicRepo = false; // This is a private repo
repositoryData = {
full_name: repoInfo.full_name,
description: repoInfo.description,
language: repoInfo.language,
visibility: 'private',
stargazers_count: repoInfo.stargazers_count,
forks_count: repoInfo.forks_count,
default_branch: repoInfo.default_branch,
size: repoInfo.size,
updated_at: repoInfo.updated_at
};
console.log(`✅ Private repository ${owner}/${repo} accessed with authentication`);
} catch (authError) {
console.log(`❌ Cannot access ${owner}/${repo} even with authentication: ${authError.message}`);
return res.status(403).json({
success: false,
message: accessCheck.error || 'Repository not accessible'
message: 'Repository not accessible - you may not have permission to access this repository'
});
}
}
// Get repository information from GitHub
const repositoryData = await githubService.fetchRepositoryMetadata(owner, repo);
// If we don't have repository data yet (private repo), fetch it with authentication
if (!repositoryData) {
repositoryData = await githubService.fetchRepositoryMetadata(owner, repo);
}
// Use the actual default branch from repository metadata if the requested branch doesn't exist
let actualBranch = branch || branch_name || repositoryData.default_branch || 'main';
@ -71,7 +234,16 @@ router.post('/attach-repository', async (req, res) => {
// Validate that the requested branch exists, fallback to default if not
try {
if (branch || branch_name) {
const octokit = await githubService.getAuthenticatedOctokit();
// Use authenticated octokit for private repos, unauthenticated for public
let octokit;
if (isPublicRepo) {
octokit = new (require('@octokit/rest')).Octokit({
userAgent: 'CodeNuk-GitIntegration/1.0.0',
});
} else {
octokit = await githubService.getAuthenticatedOctokit();
}
await octokit.git.getRef({
owner,
repo,
@ -88,7 +260,7 @@ router.post('/attach-repository', async (req, res) => {
}
// Analyze the codebase
const codebaseAnalysis = await githubService.analyzeCodebase(owner, repo, actualBranch);
const codebaseAnalysis = await githubService.analyzeCodebase(owner, repo, actualBranch, isPublicRepo);
// Store everything in PostgreSQL (without template_id)
const insertQuery = `
@ -105,31 +277,42 @@ router.post('/attach-repository', async (req, res) => {
repo,
owner,
actualBranch,
repositoryData.visibility === 'public',
isPublicRepo,
JSON.stringify(repositoryData),
JSON.stringify(codebaseAnalysis),
'synced',
accessCheck.requiresAuth,
'syncing', // Start with syncing status
!isPublicRepo, // requires_auth is true for private repos
userId || null
];
const insertResult = await database.query(insertQuery, insertValues);
const repositoryRecord = insertResult.rows[0];
// Attempt to auto-create webhook on the attached repository using OAuth token
// Attempt to auto-create webhook on the attached repository using OAuth token (only for authenticated repos)
let webhookResult = null;
if (!isPublicRepo) {
const publicBaseUrl = process.env.PUBLIC_BASE_URL || null; // e.g., your ngrok URL https://xxx.ngrok-free.app
const callbackUrl = publicBaseUrl ? `${publicBaseUrl}/api/github/webhook` : null;
const webhookResult = await githubService.ensureRepositoryWebhook(owner, repo, callbackUrl);
webhookResult = await githubService.ensureRepositoryWebhook(owner, repo, callbackUrl);
}
// Sync with fallback: try git first, then API
console.log('Syncing repository (git first, API fallback)...');
console.log(`Syncing ${isPublicRepo ? 'public' : 'private'} repository (git first, API fallback)...`);
const downloadResult = await githubService.syncRepositoryWithFallback(
owner, repo, actualBranch, repositoryRecord.id
owner, repo, actualBranch, repositoryRecord.id, isPublicRepo
);
// Update sync status based on download result
const finalSyncStatus = downloadResult.success ? 'synced' : 'error';
await database.query(
'UPDATE github_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2',
[finalSyncStatus, repositoryRecord.id]
);
if (!downloadResult.success) {
// If download failed, still return the repository record but mark the storage issue
console.warn('Repository download failed:', downloadResult.error);
} else {
console.log(`✅ Repository ${owner}/${repo} synced successfully using ${downloadResult.method} method`);
}
// Get storage information
@ -137,18 +320,25 @@ router.post('/attach-repository', async (req, res) => {
res.status(201).json({
success: true,
message: 'Repository attached successfully',
message: `Repository attached and ${downloadResult.success ? 'synced' : 'partially synced'} successfully`,
data: {
repository_id: repositoryRecord.id,
repository_name: repositoryRecord.repository_name,
owner_name: repositoryRecord.owner_name,
branch_name: repositoryRecord.branch_name,
is_public: repositoryRecord.is_public,
requires_auth: repositoryRecord.requires_auth,
is_public: isPublicRepo,
requires_auth: !isPublicRepo,
sync_status: finalSyncStatus,
metadata: repositoryData,
codebase_analysis: codebaseAnalysis,
storage_info: storageInfo,
download_result: downloadResult
download_result: downloadResult,
webhook_result: webhookResult,
authentication_info: {
is_public: isPublicRepo,
authenticated: !isPublicRepo,
github_username: null
}
}
});

View File

@ -8,7 +8,10 @@ const oauthService = new GitHubOAuthService();
// Initiate GitHub OAuth flow (supports optional user_id). If redirect=1, do 302 to GitHub.
router.get('/auth/github', async (req, res) => {
try {
const state = Math.random().toString(36).substring(7);
// If caller provided a state (e.g., containing repo context), use it; else generate one
const state = (typeof req.query.state === 'string' && req.query.state.length > 0)
? req.query.state
: Math.random().toString(36).substring(7);
const userId =
req.query.user_id ||
(req.body && req.body.user_id) ||
@ -88,10 +91,67 @@ router.get('/auth/github/callback', async (req, res) => {
console.log('[GitHub OAuth] callback about to store token for user_id =', user_id || null);
const tokenRecord = await oauthService.storeToken(accessToken, githubUser, user_id || null);
// If the OAuth state contains repo context from the attach flow, auto-attach and start sync
// State format from attach: `${random}|uid=${userId}|repo=${encodeURIComponent(url)}|branch=${encodeURIComponent(branch)}`
let autoAttach = null;
try {
if (typeof state === 'string' && state.includes('|repo=')) {
const parts = Object.fromEntries(
state.split('|').slice(1).map(kv => {
const [k, ...rest] = kv.split('=');
return [k, rest.join('=')];
})
);
const repoUrl = parts.repo ? decodeURIComponent(parts.repo) : null;
const branchName = parts.branch ? decodeURIComponent(parts.branch) : null;
if (repoUrl) {
console.log('[GitHub OAuth] Auto-attach requested for repo:', repoUrl, 'branch:', branchName || '(default)');
const GitHubIntegrationService = require('../services/github-integration.service');
const database = require('../config/database');
const githubService = new GitHubIntegrationService();
const { owner, repo, branch } = githubService.parseGitHubUrl(repoUrl);
// Get metadata using authenticated Octokit now that token is stored
const repositoryData = await githubService.fetchRepositoryMetadata(owner, repo);
let actualBranch = branchName || branch || repositoryData.default_branch || 'main';
// Attempt analysis and sync with fallback
const codebaseAnalysis = await githubService.analyzeCodebase(owner, repo, actualBranch, false);
const insertQuery = `
INSERT INTO github_repositories (
repository_url, repository_name, owner_name,
branch_name, is_public, metadata, codebase_analysis, sync_status,
requires_auth, user_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *
`;
const insertValues = [
repoUrl,
repo,
owner,
actualBranch,
repositoryData.visibility !== 'private',
JSON.stringify(repositoryData),
JSON.stringify(codebaseAnalysis),
'syncing',
repositoryData.visibility === 'private',
user_id || null,
];
const insertResult = await database.query(insertQuery, insertValues);
const repositoryRecord = insertResult.rows[0];
// Try to sync
const downloadResult = await githubService.syncRepositoryWithFallback(owner, repo, actualBranch, repositoryRecord.id, repositoryData.visibility !== 'private');
const finalSyncStatus = downloadResult.success ? 'synced' : 'error';
await database.query('UPDATE github_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2', [finalSyncStatus, repositoryRecord.id]);
autoAttach = { repository_id: repositoryRecord.id, sync_status: finalSyncStatus };
}
}
} catch (autoErr) {
console.warn('[GitHub OAuth] Auto-attach failed:', autoErr.message);
}
// Redirect back to frontend if configured
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
try {
const redirectUrl = `${frontendUrl}/project-builder?github_connected=1&user=${encodeURIComponent(githubUser.login)}`;
const redirectUrl = `${frontendUrl}/project-builder?github_connected=1&user=${encodeURIComponent(githubUser.login)}${autoAttach ? `&repo_attached=1&repository_id=${encodeURIComponent(autoAttach.repository_id)}&sync_status=${encodeURIComponent(autoAttach.sync_status)}` : ''}`;
return res.redirect(302, redirectUrl);
} catch (e) {
// Fallback to JSON if redirect fails
@ -101,7 +161,8 @@ router.get('/auth/github/callback', async (req, res) => {
data: {
github_username: githubUser.login,
github_user_id: githubUser.id,
connected_at: tokenRecord.created_at
connected_at: tokenRecord.created_at,
auto_attach: autoAttach || null
}
});
}

View File

@ -168,36 +168,71 @@ class GitHubIntegrationService {
};
}
// No token found that can access this repository
// No token found - try unauthenticated access first to check if it's public
try {
const unauthenticatedOctokit = new Octokit({
userAgent: 'CodeNuk-GitIntegration/1.0.0',
});
const { data } = await unauthenticatedOctokit.repos.get({ owner, repo });
// Repository exists and is public
return {
exists: true,
isPrivate: false,
hasAccess: true,
requiresAuth: false,
github_username: null,
token_id: null
};
} catch (unauthenticatedError) {
if (unauthenticatedError.status === 404) {
// Repository truly doesn't exist
return {
exists: false,
isPrivate: null,
hasAccess: false,
requiresAuth: false,
error: 'Repository not found'
};
} else if (unauthenticatedError.status === 401 || unauthenticatedError.status === 403) {
// Repository exists but requires authentication (private) - generate auth URL
const authUrl = await this.oauthService.generateAuthUrl(userId);
return {
exists: true,
isPrivate: true,
hasAccess: false,
requiresAuth: true,
error: 'Private repository requires authentication',
authError: false,
auth_url: authUrl
};
}
// Other error - treat as private repository requiring auth - generate auth URL
const authUrl = await this.oauthService.generateAuthUrl(userId);
return {
exists: null,
isPrivate: null,
hasAccess: false,
requiresAuth: true,
error: 'Repository not found or requires authentication',
authError: false
};
} catch (error) {
if (error.status === 404) {
return {
exists: false,
isPrivate: null,
hasAccess: false,
requiresAuth: true,
error: 'Repository not found or requires authentication'
error: 'Repository requires authentication',
authError: false,
auth_url: authUrl
};
}
// Handle authentication errors
} catch (error) {
// Handle authentication errors - generate auth URL
if (error.status === 401 || error.message.includes('token has expired') || error.message.includes('authenticate with GitHub')) {
const authUrl = await this.oauthService.generateAuthUrl(userId);
return {
exists: null,
isPrivate: null,
hasAccess: false,
requiresAuth: true,
error: 'GitHub authentication required or token expired',
authError: true
authError: true,
auth_url: authUrl
};
}
@ -249,9 +284,19 @@ class GitHubIntegrationService {
}
// Analyze codebase structure
async analyzeCodebase(owner, repo, branch) {
async analyzeCodebase(owner, repo, branch, isPublicRepo = false) {
try {
const octokit = await this.getAuthenticatedOctokit();
// Use appropriate octokit instance based on repository type
let octokit;
if (isPublicRepo) {
// For public repos, use unauthenticated octokit
octokit = new Octokit({
userAgent: 'CodeNuk-GitIntegration/1.0.0',
});
} else {
// For private repos, use authenticated octokit
octokit = await this.getAuthenticatedOctokit();
}
// Get the commit SHA for the branch
const { data: ref } = await octokit.git.getRef({
@ -394,7 +439,7 @@ class GitHubIntegrationService {
}
// Git-based: clone or update local repo and re-index into DB
async syncRepositoryWithGit(owner, repo, branch, repositoryId) {
async syncRepositoryWithGit(owner, repo, branch, repositoryId, isPublicRepo = false) {
const database = require('../config/database');
const localPath = this.gitRepoService.getLocalRepoPath(owner, repo, branch);
let storageRecord = null;
@ -408,8 +453,16 @@ class GitHubIntegrationService {
localPath
);
// Clone if missing (prefer authenticated HTTPS with OAuth token), otherwise fetch & fast-forward
// Clone if missing (prefer authenticated HTTPS with OAuth token for private repos, public for public repos)
let repoPath = null;
if (isPublicRepo) {
// For public repos, try unauthenticated clone first
try {
repoPath = await this.gitRepoService.cloneIfMissing(owner, repo, branch);
} catch (error) {
console.warn(`Failed to clone public repo without auth: ${error.message}`);
// Fallback to authenticated clone if available
try {
const tokenRecord = await this.oauthService.getToken();
if (tokenRecord?.access_token) {
@ -423,9 +476,33 @@ class GitHubIntegrationService {
);
}
} catch (_) {}
}
} else {
// For private repos, try authenticated clone first
try {
const tokenRecord = await this.oauthService.getToken();
if (tokenRecord?.access_token) {
repoPath = await this.gitRepoService.cloneIfMissingWithAuth(
owner,
repo,
branch,
'github.com',
tokenRecord.access_token,
'oauth2'
);
}
} catch (_) {}
// Fallback to unauthenticated clone (will likely fail for private repos)
if (!repoPath) {
repoPath = await this.gitRepoService.cloneIfMissing(owner, repo, branch);
}
}
if (!repoPath) {
throw new Error('Failed to clone repository');
}
const beforeSha = await this.gitRepoService.getHeadSha(repoPath);
const { afterSha } = await this.gitRepoService.fetchAndFastForward(repoPath, branch);
@ -563,15 +640,15 @@ class GitHubIntegrationService {
}
// Try git-based sync first, fall back to GitHub API download on failure
async syncRepositoryWithFallback(owner, repo, branch, repositoryId) {
async syncRepositoryWithFallback(owner, repo, branch, repositoryId, isPublicRepo = false) {
// First attempt: full git clone/fetch and index
const gitResult = await this.syncRepositoryWithGit(owner, repo, branch, repositoryId);
const gitResult = await this.syncRepositoryWithGit(owner, repo, branch, repositoryId, isPublicRepo);
if (gitResult && gitResult.success) {
return { method: 'git', ...gitResult };
}
// Fallback: API-based download and storage
const apiResult = await this.downloadRepositoryWithStorage(owner, repo, branch, repositoryId);
const apiResult = await this.downloadRepositoryWithStorage(owner, repo, branch, repositoryId, isPublicRepo);
if (apiResult && apiResult.success) {
return { method: 'api', ...apiResult, git_error: gitResult?.error };
}
@ -580,7 +657,7 @@ class GitHubIntegrationService {
}
// Download repository files locally and store in database
async downloadRepositoryWithStorage(owner, repo, branch, repositoryId) {
async downloadRepositoryWithStorage(owner, repo, branch, repositoryId, isPublicRepo = false) {
const targetDir = path.join(
process.env.ATTACHED_REPOS_DIR,
`${owner}__${repo}__${branch}`
@ -600,7 +677,17 @@ class GitHubIntegrationService {
targetDir
);
const octokit = await this.getAuthenticatedOctokit();
// Use appropriate octokit instance based on repository type
let octokit;
if (isPublicRepo) {
// For public repos, use unauthenticated octokit
octokit = new Octokit({
userAgent: 'CodeNuk-GitIntegration/1.0.0',
});
} else {
// For private repos, use authenticated octokit
octokit = await this.getAuthenticatedOctokit();
}
// Get the commit SHA for the branch
const { data: ref } = await octokit.git.getRef({

View File

@ -40,6 +40,12 @@ class GitHubOAuthService {
return `https://github.com/login/oauth/authorize?${params.toString()}`;
}
// Generate auth URL for a specific user (wrapper method)
async generateAuthUrl(userId) {
const state = Math.random().toString(36).substring(7);
return this.getAuthUrl(state, userId);
}
// Exchange authorization code for access token
async exchangeCodeForToken(code) {
const response = await fetch('https://github.com/login/oauth/access_token', {