-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(import): improve GitHub repository import process
- Loading branch information
1 parent
f55c9d6
commit 3a0cce0
Showing
5 changed files
with
108 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,116 +1,83 @@ | ||
import { AppConfig } from '@/config/AppConfig'; | ||
import { Tree } from '@/interfaces/workspace.interface'; | ||
import fileSystem from '@/lib/fs'; | ||
import ZIP from '@/lib/zip'; | ||
import axios from 'axios'; | ||
|
||
function validateAndTransformPath(repoURL: string): string { | ||
const url = new URL(repoURL); | ||
const pathnameParts = url.pathname.split('/'); | ||
async function convertToZipUrl( | ||
gitUrl: string, | ||
): Promise<{ url: string; path: string }> { | ||
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<ResultItem[]> { | ||
const result: ResultItem[] = []; | ||
export async function downloadRepo(repoURL: string): Promise<Tree[]> { | ||
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<Tree[]> { | ||
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; | ||
}, []); | ||
} |