From 3a0cce0095e96a6fb8e6e0a9d3160ad7cb6c7da3 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Fri, 11 Oct 2024 03:05:21 +0530 Subject: [PATCH] feat(import): improve GitHub repository import process --- .../project/NewProject/NewProject.tsx | 9 +- .../project/ManageProject/ManageProject.tsx | 13 +- src/lib/fs.ts | 6 +- src/lib/zip.ts | 27 ++- src/utility/gitRepoDownloader.ts | 167 +++++++----------- 5 files changed, 108 insertions(+), 114 deletions(-) diff --git a/src/components/project/NewProject/NewProject.tsx b/src/components/project/NewProject/NewProject.tsx index 9c31775..c01e271 100644 --- a/src/components/project/NewProject/NewProject.tsx +++ b/src/components/project/NewProject/NewProject.tsx @@ -10,6 +10,7 @@ import { } from '@/interfaces/workspace.interface'; import { Analytics } from '@/utility/analytics'; import EventEmitter from '@/utility/eventEmitter'; +import { downloadRepo } from '@/utility/gitRepoDownloader'; import { decodeBase64 } from '@/utility/utils'; import { Button, Form, Input, Modal, Radio, Upload, message } from 'antd'; import { useForm } from 'antd/lib/form/Form'; @@ -85,17 +86,13 @@ const NewProject: FC = ({ const onFormFinish = async (values: FormValues) => { const { githubUrl, language } = values; const { name: projectName } = values; - const files: Tree[] = defaultFiles; + let files: Tree[] = defaultFiles; try { setIsLoading(true); if (projectType === 'git') { - throw new Error( - `Git import has been disabled for now. Repo: ${githubUrl}`, - ); - // TODO: Implement downloadRepo function - // files = await downloadRepo(githubUrl as string); + files = await downloadRepo(githubUrl as string); } await createProject({ diff --git a/src/components/workspace/project/ManageProject/ManageProject.tsx b/src/components/workspace/project/ManageProject/ManageProject.tsx index 5fb4bc3..9c7f0b3 100644 --- a/src/components/workspace/project/ManageProject/ManageProject.tsx +++ b/src/components/workspace/project/ManageProject/ManageProject.tsx @@ -10,12 +10,14 @@ import { baseProjectPath, useProject } from '@/hooks/projectV2.hooks'; import { Project } from '@/interfaces/workspace.interface'; import EventEmitter from '@/utility/eventEmitter'; import { Button, Modal, Select, message } from 'antd'; -import Router from 'next/router'; +import Router, { useRouter } from 'next/router'; import { FC, useEffect, useState } from 'react'; import s from './ManageProject.module.scss'; const ManageProject: FC = () => { const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false); + const router = useRouter(); + const { importURL } = router.query; const { projects, @@ -50,7 +52,14 @@ const ManageProject: FC = () => {
- + { + try { + const cleanedUrl = gitUrl.replace(/\/$/, ''); + const regex = /github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+))?/; + const match = cleanedUrl.match(regex); + let pathName = ''; - if (pathnameParts.length >= 3) { - if (pathnameParts[3] === 'tree' && pathnameParts.length >= 6) { - // GitHub path with /tree, so transform it to API URL - const apiUrl = `https://api.github.com/repos/${pathnameParts[1]}/${ - pathnameParts[2] - }/contents/${pathnameParts.slice(5).join('/')}`; - return apiUrl; - } else { - // GitHub path without /tree, so transform it to API URL with the default branch (e.g., main) - const apiUrl = `https://api.github.com/repos/${pathnameParts[1]}/${ - pathnameParts[2] - }/contents/${pathnameParts.slice(3).join('/')}`; - return apiUrl; + if (cleanedUrl.includes('/tree/') && cleanedUrl.split('/').length >= 6) { + pathName = cleanedUrl.split('tree/')[1].split('/').slice(1).join('/'); } - } - // Invalid path format - throw new Error('Invalid GitHub path format. Please provide a valid path.'); -} + if (match) { + const [_, owner, repo, branch] = match; -// Function to convert GitHub API response to custom format -interface DataItem { - type: 'file' | 'dir'; - name: string; - sha: string; - download_url?: string; - url: string; -} + // Fetch the default branch if it's not provided in the URL + let branchName = branch; + if (!branchName) { + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}`, + ); + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + errorData.message || 'Failed to fetch repository details', + ); + } + const repoData = await response.json(); + branchName = repoData.default_branch; + } -interface ResultItem { - id: string; - name: string; - parent: string | null; - type: 'file' | 'directory'; - path: string; - content?: string; - isOpen?: boolean; + const zipUrl = `https://github.com/${owner}/${repo}/archive/refs/heads/${branchName}.zip`; + return { url: zipUrl, path: pathName }; + } + + throw new Error('Invalid GitHub URL format'); + } catch (error) { + throw new Error( + (error as Error).message || + 'Invalid GitHub URL or failed to fetch repository details', + ); + } } -async function convertToCustomFormat( - data: DataItem[], - parent: string | null = null, - parentPath: string = '', -): Promise { - const result: ResultItem[] = []; +export async function downloadRepo(repoURL: string): Promise { + const { url, path } = await convertToZipUrl(repoURL); + if (!url) { + throw new Error('Invalid GitHub URL'); + } + + const zipResponse = await axios.get(`${AppConfig.proxy.url}${url}`, { + headers: { 'x-cors-api-key': AppConfig.proxy.key }, + responseType: 'arraybuffer', + }); - for (const item of data) { - if (item.type === 'file') { - if (!item.download_url) continue; - const response = await fetch(item.download_url); - const content = await response.text(); - const fileName = parentPath ? parentPath + '/' + item.name : item.name; - result.push({ - id: item.sha, - name: item.name, - parent: parent, - type: 'file', - path: fileName, - content: content, - }); - } else { - const dirName = parentPath ? parentPath + '/' + item.name : item.name; - result.push({ - id: item.sha, - name: item.name, - parent: parent, - type: 'directory', - isOpen: false, - path: dirName, - }); + const blob = new Blob([zipResponse.data], { type: 'application/zip' }); + const filesData = await new ZIP(fileSystem).importZip(blob, '', false); - // Fetch subdirectory contents and add them as siblings to the current directory - const subDirContents = await getDirContents(item.url); - const subDirItems = await convertToCustomFormat( - subDirContents, - item.sha, - dirName, - ); - result.push(...subDirItems); - } + if (filesData.length === 0) { + throw new Error('No files found in the repository'); } - return result; -} - -// Function to fetch directory contents from GitHub API -async function getDirContents(url: string) { - const response = await fetch(url); - return await response.json(); -} + return filesData.reduce((acc: Tree[], item) => { + // Remove the repo name from the path. Ex. /repo-name/file.ts -> /file.ts + item.path = item.path.split('/').slice(2).join('/'); -export async function downloadRepo(repoURL: string): Promise { - const apiUrl = validateAndTransformPath(repoURL); - const data = await getDirContents(apiUrl); - try { - const jsonData = await convertToCustomFormat(data); - return jsonData as Tree[]; - } catch (error) { - if (error instanceof Error) { - switch (error.message) { - case 'data is not iterable': - throw new Error('Repository not found.'); - default: - throw new Error(error.message); - } - } else { - throw new Error('An unexpected error occurred.'); + if (path && item.path.startsWith(path)) { + item.path = item.path.replace(path, '').replace(/^\/+/, ''); // Remove leading '/' + acc.push({ ...item, type: 'file' }); // Add modified item to accumulator + } else if (!path) { + acc.push({ ...item, type: 'file' }); // Add unmodified item if no path is provided } - } + + return acc; + }, []); }