mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-03-10 14:13:19 +00:00
182 lines
5.3 KiB
TypeScript
182 lines
5.3 KiB
TypeScript
import ignore from 'ignore';
|
|
import { useGit } from '~/lib/hooks/useGit';
|
|
import type { Message } from 'ai';
|
|
import { detectProjectCommands, createCommandsMessage, escapeBoltTags } from '~/utils/projectCommands';
|
|
import { generateId } from '~/utils/fileUtils';
|
|
import { useState } from 'react';
|
|
import { toast } from 'react-toastify';
|
|
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
|
|
import { RepositorySelectionDialog } from '~/components/@settings/tabs/connections/components/RepositorySelectionDialog';
|
|
import { classNames } from '~/utils/classNames';
|
|
import { Button } from '~/components/ui/Button';
|
|
import type { IChatMetadata } from '~/lib/persistence/db';
|
|
|
|
const IGNORE_PATTERNS = [
|
|
'node_modules/**',
|
|
'.git/**',
|
|
'.github/**',
|
|
'.vscode/**',
|
|
'dist/**',
|
|
'build/**',
|
|
'.next/**',
|
|
'coverage/**',
|
|
'.cache/**',
|
|
'.idea/**',
|
|
'**/*.log',
|
|
'**/.DS_Store',
|
|
'**/npm-debug.log*',
|
|
'**/yarn-debug.log*',
|
|
'**/yarn-error.log*',
|
|
'**/*lock.json',
|
|
'**/*lock.yaml',
|
|
];
|
|
|
|
const ig = ignore().add(IGNORE_PATTERNS);
|
|
|
|
const MAX_FILE_SIZE = 100 * 1024; // 100KB limit per file
|
|
const MAX_TOTAL_SIZE = 500 * 1024; // 500KB total limit
|
|
|
|
interface GitCloneButtonProps {
|
|
className?: string;
|
|
importChat?: (description: string, messages: Message[], metadata?: IChatMetadata) => Promise<void>;
|
|
}
|
|
|
|
export default function GitCloneButton({ importChat, className }: GitCloneButtonProps) {
|
|
const { ready, gitClone } = useGit();
|
|
const [loading, setLoading] = useState(false);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
|
|
const handleClone = async (repoUrl: string) => {
|
|
if (!ready) {
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const { workdir, data } = await gitClone(repoUrl);
|
|
|
|
if (importChat) {
|
|
const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
|
|
const textDecoder = new TextDecoder('utf-8');
|
|
|
|
let totalSize = 0;
|
|
const skippedFiles: string[] = [];
|
|
const fileContents = [];
|
|
|
|
for (const filePath of filePaths) {
|
|
const { data: content, encoding } = data[filePath];
|
|
|
|
// Skip binary files
|
|
if (
|
|
content instanceof Uint8Array &&
|
|
!filePath.match(/\.(txt|md|astro|mjs|js|jsx|ts|tsx|json|html|css|scss|less|yml|yaml|xml|svg)$/i)
|
|
) {
|
|
skippedFiles.push(filePath);
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const textContent =
|
|
encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '';
|
|
|
|
if (!textContent) {
|
|
continue;
|
|
}
|
|
|
|
// Check file size
|
|
const fileSize = new TextEncoder().encode(textContent).length;
|
|
|
|
if (fileSize > MAX_FILE_SIZE) {
|
|
skippedFiles.push(`${filePath} (too large: ${Math.round(fileSize / 1024)}KB)`);
|
|
continue;
|
|
}
|
|
|
|
// Check total size
|
|
if (totalSize + fileSize > MAX_TOTAL_SIZE) {
|
|
skippedFiles.push(`${filePath} (would exceed total size limit)`);
|
|
continue;
|
|
}
|
|
|
|
totalSize += fileSize;
|
|
fileContents.push({
|
|
path: filePath,
|
|
content: textContent,
|
|
});
|
|
} catch (e: any) {
|
|
skippedFiles.push(`${filePath} (error: ${e.message})`);
|
|
}
|
|
}
|
|
|
|
const commands = await detectProjectCommands(fileContents);
|
|
const commandsMessage = createCommandsMessage(commands);
|
|
|
|
const filesMessage: Message = {
|
|
role: 'assistant',
|
|
content: `Cloning the repo ${repoUrl} into ${workdir}
|
|
${
|
|
skippedFiles.length > 0
|
|
? `\nSkipped files (${skippedFiles.length}):
|
|
${skippedFiles.map((f) => `- ${f}`).join('\n')}`
|
|
: ''
|
|
}
|
|
|
|
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
|
${fileContents
|
|
.map(
|
|
(file) =>
|
|
`<boltAction type="file" filePath="${file.path}">
|
|
${escapeBoltTags(file.content)}
|
|
</boltAction>`,
|
|
)
|
|
.join('\n')}
|
|
</boltArtifact>`,
|
|
id: generateId(),
|
|
createdAt: new Date(),
|
|
};
|
|
|
|
const messages = [filesMessage];
|
|
|
|
if (commandsMessage) {
|
|
messages.push(commandsMessage);
|
|
}
|
|
|
|
await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error during import:', error);
|
|
toast.error('Failed to import repository');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Button
|
|
onClick={() => setIsDialogOpen(true)}
|
|
title="Clone a Git Repo"
|
|
variant="outline"
|
|
size="lg"
|
|
className={classNames(
|
|
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
|
|
'text-bolt-elements-textPrimary dark:text-white',
|
|
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
|
|
'border-[#E5E5E5] dark:border-[#333333]',
|
|
'h-10 px-4 py-2 min-w-[120px] justify-center',
|
|
'transition-all duration-200 ease-in-out',
|
|
className,
|
|
)}
|
|
disabled={!ready || loading}
|
|
>
|
|
<span className="i-ph:git-branch w-4 h-4" />
|
|
Clone a Git Repo
|
|
</Button>
|
|
|
|
<RepositorySelectionDialog isOpen={isDialogOpen} onClose={() => setIsDialogOpen(false)} onSelect={handleClone} />
|
|
|
|
{loading && <LoadingOverlay message="Please wait while we clone the repository..." />}
|
|
</>
|
|
);
|
|
}
|