Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: introduce our own cors proxy for git import to fix 403 errors on isometric git cors proxy #924

Merged
merged 3 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 102 additions & 43 deletions app/components/chat/GitCloneButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { useGit } from '~/lib/hooks/useGit';
import type { Message } from 'ai';
import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
import { generateId } from '~/utils/fileUtils';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';

const IGNORE_PATTERNS = [
'node_modules/**',
Expand Down Expand Up @@ -37,6 +40,10 @@ interface GitCloneButtonProps {

export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
const { ready, gitClone } = useGit();
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(0);
const [progressText, setProgressText] = useState('');

const onClick = async (_e: any) => {
if (!ready) {
return;
Expand All @@ -45,33 +52,66 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
const repoUrl = prompt('Enter the Git url');

if (repoUrl) {
const { workdir, data } = await gitClone(repoUrl);

if (importChat) {
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
console.log(filePaths);

const textDecoder = new TextDecoder('utf-8');

// Convert files to common format for command detection
const fileContents = filePaths
.map((filePath) => {
const { data: content, encoding } = data[filePath];
return {
path: filePath,
content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
};
})
.filter((f) => f.content);

// Detect and create commands message
const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands);

// Create files message
const filesMessage: Message = {
role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir}
setLoading(true);

try {
const { workdir, data } = await gitClone(repoUrl, {
corsProxy: '/api/git-proxy',
onProgress: (event) => {
let percent;
let fsPercent;

switch (event.phase) {
case 'counting':
setProgress(5);
setProgressText(`Counting objects: ${event.loaded}...`);
break;
case 'receiving':
percent = event.total ? (event.loaded / event.total) * 50 + 10 : 10;
setProgress(percent);
setProgressText(`Receiving objects: ${event.loaded}${event.total ? `/${event.total}` : ''}...`);
break;
case 'resolving':
setProgress(65);
setProgressText('Resolving deltas...');
break;
case 'fs_operations':
fsPercent = event.total ? (event.loaded / event.total) * 25 + 70 : 70;
setProgress(fsPercent);
setProgressText(`Processing files: ${event.loaded}/${event.total}`);
break;
}
},
});

if (importChat) {
setProgress(95);
setProgressText('Processing repository...');

const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
console.log(filePaths);

const textDecoder = new TextDecoder('utf-8');

const fileContents = filePaths
.map((filePath) => {
const { data: content, encoding } = data[filePath];
return {
path: filePath,
content:
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
};
})
.filter((f) => f.content);

setProgressText('Analyzing project structure...');

const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands);

const filesMessage: Message = {
role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir}
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
${fileContents
.map(
Expand All @@ -82,29 +122,48 @@ ${file.content}
)
.join('\n')}
</boltArtifact>`,
id: generateId(),
createdAt: new Date(),
};
id: generateId(),
createdAt: new Date(),
};

const messages = [filesMessage];
const messages = [filesMessage];

if (commandsMessage) {
messages.push(commandsMessage);
}
if (commandsMessage) {
messages.push(commandsMessage);
}

await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
setProgress(98);
setProgressText('Finalizing import...');
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
setProgress(100);
setProgressText('Import complete!');
}
} catch (error) {
console.error('Error during import:', error);
toast.error('Failed to import repository');
} finally {
setLoading(false);
}
}
};

return (
<button
onClick={onClick}
title="Clone a Git Repo"
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
>
<span className="i-ph:git-branch" />
Clone a Git Repo
</button>
<>
<button
onClick={onClick}
title="Clone a Git Repo"
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
>
<span className="i-ph:git-branch" />
Clone a Git Repo
</button>
{loading && (
<LoadingOverlay
message="Please wait while we clone the repository..."
progress={progress}
progressText={progressText}
/>
)}
</>
);
}
124 changes: 89 additions & 35 deletions app/components/git/GitUrlImport.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,41 +41,77 @@ export function GitUrlImport() {
const { ready: gitReady, gitClone } = useGit();
const [imported, setImported] = useState(false);
const [loading, setLoading] = useState(true);
const [progress, setProgress] = useState(0);
const [progressText, setProgressText] = useState('');

const importRepo = async (repoUrl?: string) => {
if (!gitReady && !historyReady) {
return;
}

if (repoUrl) {
setProgress(0);
setProgressText('Initializing clone...');

const ig = ignore().add(IGNORE_PATTERNS);
const { workdir, data } = await gitClone(repoUrl);

if (importChat) {
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));

const textDecoder = new TextDecoder('utf-8');

// Convert files to common format for command detection
const fileContents = filePaths
.map((filePath) => {
const { data: content, encoding } = data[filePath];
return {
path: filePath,
content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
};
})
.filter((f) => f.content);

// Detect and create commands message
const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands);

// Create files message
const filesMessage: Message = {
role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir}
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">

try {
const { workdir, data } = await gitClone(repoUrl, {
corsProxy: '/api/git-proxy',
onProgress: (event) => {
let percent;
let fsPercent;

switch (event.phase) {
case 'counting':
setProgress(5);
setProgressText(`Counting objects: ${event.loaded}...`);
break;
case 'receiving':
percent = event.total ? (event.loaded / event.total) * 50 + 10 : 10;
setProgress(percent);
setProgressText(`Receiving objects: ${event.loaded}${event.total ? `/${event.total}` : ''}...`);
break;
case 'resolving':
setProgress(65);
setProgressText('Resolving deltas...');
break;
case 'fs_operations':
fsPercent = event.total ? (event.loaded / event.total) * 25 + 70 : 70;
setProgress(fsPercent);
setProgressText(`Processing files: ${event.loaded}/${event.total}`);
break;
}
},
});

if (importChat) {
setProgress(95);
setProgressText('Processing repository...');

const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
const textDecoder = new TextDecoder('utf-8');

const fileContents = filePaths
.map((filePath) => {
const { data: content, encoding } = data[filePath];
return {
path: filePath,
content:
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
};
})
.filter((f) => f.content);

setProgressText('Analyzing project structure...');

const commands = await detectProjectCommands(fileContents);
const commandsMessage = createCommandsMessage(commands);

const filesMessage: Message = {
role: 'assistant',
content: `Cloning the repo ${repoUrl} into ${workdir}
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
${fileContents
.map(
(file) =>
Expand All @@ -85,17 +121,29 @@ ${file.content}
)
.join('\n')}
</boltArtifact>`,
id: generateId(),
createdAt: new Date(),
};
id: generateId(),
createdAt: new Date(),
};

const messages = [filesMessage];

const messages = [filesMessage];
if (commandsMessage) {
messages.push(commandsMessage);
}

if (commandsMessage) {
messages.push(commandsMessage);
setProgress(98);
setProgressText('Finalizing import...');
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
setProgress(100);
setProgressText('Import complete!');
}
} catch (error) {
console.error('Error during import:', error);
toast.error('Failed to import repository');
setLoading(false);
window.location.href = '/';

await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
return;
}
}
};
Expand Down Expand Up @@ -126,7 +174,13 @@ ${file.content}
{() => (
<>
<Chat />
{loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
{loading && (
<LoadingOverlay
message="Please wait while we clone the repository..."
progress={progress}
progressText={progressText}
/>
)}
</>
)}
</ClientOnly>
Expand Down
22 changes: 20 additions & 2 deletions app/components/ui/LoadingOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
export const LoadingOverlay = ({ message = 'Loading...' }) => {
export const LoadingOverlay = ({
message = 'Loading...',
progress,
progressText,
}: {
message?: string;
progress?: number;
progressText?: string;
}) => {
return (
<div className="fixed inset-0 flex items-center justify-center bg-black/80 z-50 backdrop-blur-sm">
{/* Loading content */}
<div className="relative flex flex-col items-center gap-4 p-8 rounded-lg bg-bolt-elements-background-depth-2 shadow-lg">
<div
className={'i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress'}
style={{ fontSize: '2rem' }}
></div>
<p className="text-lg text-bolt-elements-textTertiary">{message}</p>
{progress !== undefined && (
<div className="w-64 flex flex-col gap-2">
<div className="w-full h-2 bg-bolt-elements-background-depth-1 rounded-full overflow-hidden">
<div
className="h-full bg-bolt-elements-loader-progress transition-all duration-300 ease-out rounded-full"
style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}
/>
</div>
{progressText && <p className="text-sm text-bolt-elements-textTertiary text-center">{progressText}</p>}
</div>
)}
</div>
</div>
);
Expand Down
Loading
Loading