|
|
|
|
@ -60,19 +60,36 @@ function parseGitUrl(url: string): { platform: string; owner: string; repo: stri
@@ -60,19 +60,36 @@ function parseGitUrl(url: string): { platform: string; owner: string; repo: stri
|
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// GitLab
|
|
|
|
|
const gitlabMatch = url.match(/gitlab\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/); |
|
|
|
|
// GitLab (both gitlab.com and self-hosted instances)
|
|
|
|
|
const gitlabMatch = url.match(/(https?:\/\/[^/]*gitlab[^/]*)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/); |
|
|
|
|
if (gitlabMatch) { |
|
|
|
|
const baseHost = gitlabMatch[1]; |
|
|
|
|
// GitLab.com uses /api/v4, self-hosted might use /api/v4 or /api/v3
|
|
|
|
|
const baseUrl = baseHost.includes('gitlab.com')
|
|
|
|
|
? 'https://gitlab.com/api/v4' |
|
|
|
|
: `${baseHost}/api/v4`; |
|
|
|
|
return { |
|
|
|
|
platform: 'gitlab', |
|
|
|
|
owner: gitlabMatch[1], |
|
|
|
|
repo: gitlabMatch[2].replace(/\.git$/, ''), |
|
|
|
|
baseUrl: 'https://gitlab.com/api/v4' |
|
|
|
|
owner: gitlabMatch[2], |
|
|
|
|
repo: gitlabMatch[3].replace(/\.git$/, ''), |
|
|
|
|
baseUrl |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Gitea (generic pattern)
|
|
|
|
|
const giteaMatch = url.match(/(https?:\/\/[^/]+)[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/); |
|
|
|
|
// OneDev (detect by onedev in hostname)
|
|
|
|
|
const onedevMatch = url.match(/(https?:\/\/[^/]*onedev[^/]*)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/); |
|
|
|
|
if (onedevMatch) { |
|
|
|
|
return { |
|
|
|
|
platform: 'onedev', |
|
|
|
|
owner: onedevMatch[2], |
|
|
|
|
repo: onedevMatch[3].replace(/\.git$/, ''), |
|
|
|
|
baseUrl: `${onedevMatch[1]}/api/v1` // OneDev uses /api/v1 similar to Gitea
|
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Gitea and other Git hosting services (generic pattern) - matches https://host/owner/repo.git or https://host/owner/repo
|
|
|
|
|
// This is a catch-all for services that use Gitea-compatible API (like Gitea, Forgejo, etc.)
|
|
|
|
|
const giteaMatch = url.match(/(https?:\/\/[^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/); |
|
|
|
|
if (giteaMatch) { |
|
|
|
|
return { |
|
|
|
|
platform: 'gitea', |
|
|
|
|
@ -418,41 +435,100 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
@@ -418,41 +435,100 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
|
|
|
|
|
*/ |
|
|
|
|
async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Promise<GitRepoInfo | null> { |
|
|
|
|
try { |
|
|
|
|
const [repoData, branchesData, commitsData] = await Promise.all([ |
|
|
|
|
fetch(`${baseUrl}/repos/${owner}/${repo}`).then(r => r.json()), |
|
|
|
|
fetch(`${baseUrl}/repos/${owner}/${repo}/branches`).then(r => r.json()), |
|
|
|
|
fetch(`${baseUrl}/repos/${owner}/${repo}/commits?limit=10`).then(r => r.json()) |
|
|
|
|
const repoResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}`); |
|
|
|
|
if (!repoResponse.ok) { |
|
|
|
|
console.warn(`Gitea API error for repo ${owner}/${repo}: ${repoResponse.status} ${repoResponse.statusText}`); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
const repoData = await repoResponse.json(); |
|
|
|
|
|
|
|
|
|
const defaultBranch = repoData.default_branch || 'master'; |
|
|
|
|
|
|
|
|
|
const [branchesResponse, commitsResponse] = await Promise.all([ |
|
|
|
|
fetch(`${baseUrl}/repos/${owner}/${repo}/branches`).catch(() => null), |
|
|
|
|
fetch(`${baseUrl}/repos/${owner}/${repo}/commits?limit=10`).catch(() => null) |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
const branches: GitBranch[] = branchesData.map((b: any) => ({ |
|
|
|
|
name: b.name, |
|
|
|
|
commit: { |
|
|
|
|
sha: b.commit.id, |
|
|
|
|
message: b.commit.message.split('\n')[0], |
|
|
|
|
author: b.commit.author.name, |
|
|
|
|
date: b.commit.timestamp |
|
|
|
|
|
|
|
|
|
let branchesData: any[] = []; |
|
|
|
|
let commitsData: any[] = []; |
|
|
|
|
|
|
|
|
|
if (branchesResponse && branchesResponse.ok) { |
|
|
|
|
branchesData = await branchesResponse.json(); |
|
|
|
|
if (!Array.isArray(branchesData)) { |
|
|
|
|
console.warn('Gitea branches response is not an array:', branchesData); |
|
|
|
|
branchesData = []; |
|
|
|
|
} |
|
|
|
|
})); |
|
|
|
|
} else { |
|
|
|
|
console.warn(`Gitea API error for branches: ${branchesResponse?.status || 'unknown'}`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (commitsResponse && commitsResponse.ok) { |
|
|
|
|
commitsData = await commitsResponse.json(); |
|
|
|
|
if (!Array.isArray(commitsData)) { |
|
|
|
|
console.warn('Gitea commits response is not an array:', commitsData); |
|
|
|
|
commitsData = []; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
console.warn(`Gitea API error for commits: ${commitsResponse?.status || 'unknown'}`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const commits: GitCommit[] = commitsData.map((c: any) => ({ |
|
|
|
|
sha: c.sha, |
|
|
|
|
message: c.commit.message.split('\n')[0], |
|
|
|
|
author: c.commit.author.name, |
|
|
|
|
date: c.commit.timestamp |
|
|
|
|
})); |
|
|
|
|
const branches: GitBranch[] = branchesData.map((b: any) => { |
|
|
|
|
const commitObj = b.commit || {}; |
|
|
|
|
return { |
|
|
|
|
name: b.name || '', |
|
|
|
|
commit: { |
|
|
|
|
sha: commitObj.id || b.commit?.sha || '', |
|
|
|
|
message: commitObj.message ? commitObj.message.split('\n')[0] : 'No commit message', |
|
|
|
|
author: commitObj.author?.name || commitObj.author_name || 'Unknown', |
|
|
|
|
date: commitObj.timestamp || commitObj.created || new Date().toISOString() |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Fetch file tree
|
|
|
|
|
const commits: GitCommit[] = commitsData.map((c: any) => { |
|
|
|
|
const commitObj = c.commit || {}; |
|
|
|
|
return { |
|
|
|
|
sha: c.sha || c.id || '', |
|
|
|
|
message: commitObj.message ? commitObj.message.split('\n')[0] : 'No commit message', |
|
|
|
|
author: commitObj.author?.name || commitObj.author_name || 'Unknown', |
|
|
|
|
date: commitObj.timestamp || commitObj.created || new Date().toISOString() |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Fetch file tree - Gitea uses /git/trees API endpoint
|
|
|
|
|
let files: GitFile[] = []; |
|
|
|
|
try { |
|
|
|
|
const treeData = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents?ref=${repoData.default_branch}`).then(r => r.json()); |
|
|
|
|
files = treeData.map((item: any) => ({ |
|
|
|
|
name: item.name, |
|
|
|
|
path: item.path, |
|
|
|
|
type: item.type === 'dir' ? 'dir' : 'file', |
|
|
|
|
size: item.size |
|
|
|
|
})); |
|
|
|
|
} catch { |
|
|
|
|
// Tree fetch failed
|
|
|
|
|
// Try the git/trees endpoint first (more complete)
|
|
|
|
|
const treeResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}/git/trees/${defaultBranch}?recursive=1`).catch(() => null); |
|
|
|
|
if (treeResponse && treeResponse.ok) { |
|
|
|
|
const treeData = await treeResponse.json(); |
|
|
|
|
if (treeData.tree && Array.isArray(treeData.tree)) { |
|
|
|
|
files = treeData.tree |
|
|
|
|
.filter((item: any) => item.type === 'blob' || item.type === 'tree') |
|
|
|
|
.map((item: any) => ({ |
|
|
|
|
name: item.path.split('/').pop() || item.path, |
|
|
|
|
path: item.path, |
|
|
|
|
type: item.type === 'tree' ? 'dir' : 'file', |
|
|
|
|
size: item.size |
|
|
|
|
})); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
// Fallback to contents endpoint (only root directory)
|
|
|
|
|
const contentsResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents?ref=${defaultBranch}`).catch(() => null); |
|
|
|
|
if (contentsResponse && contentsResponse.ok) { |
|
|
|
|
const contentsData = await contentsResponse.json(); |
|
|
|
|
if (Array.isArray(contentsData)) { |
|
|
|
|
files = contentsData.map((item: any) => ({ |
|
|
|
|
name: item.name, |
|
|
|
|
path: item.path || item.name, |
|
|
|
|
type: item.type === 'dir' ? 'dir' : 'file', |
|
|
|
|
size: item.size |
|
|
|
|
})); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn('Failed to fetch Gitea file tree:', error); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Try to fetch README (prioritize .adoc over .md)
|
|
|
|
|
@ -461,11 +537,11 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
@@ -461,11 +537,11 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
|
|
|
|
|
const readmeFiles = ['README.adoc', 'README.md', 'README.rst', 'README.txt']; |
|
|
|
|
for (const readmeFile of readmeFiles) { |
|
|
|
|
try { |
|
|
|
|
const fileData = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents/${readmeFile}?ref=${repoData.default_branch}`).then(r => { |
|
|
|
|
if (!r.ok) throw new Error('Not found'); |
|
|
|
|
return r.json(); |
|
|
|
|
}); |
|
|
|
|
const fileResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents/${readmeFile}?ref=${defaultBranch}`); |
|
|
|
|
if (!fileResponse.ok) throw new Error('Not found'); |
|
|
|
|
const fileData = await fileResponse.json(); |
|
|
|
|
if (fileData.content) { |
|
|
|
|
// Gitea returns base64 encoded content
|
|
|
|
|
const content = atob(fileData.content.replace(/\s/g, '')); |
|
|
|
|
readme = { |
|
|
|
|
path: readmeFile, |
|
|
|
|
@ -474,8 +550,9 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
@@ -474,8 +550,9 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
|
|
|
|
|
}; |
|
|
|
|
break; // Found a README, stop searching
|
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
continue; // Try next file
|
|
|
|
|
} catch (error) { |
|
|
|
|
// Try next file
|
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -499,11 +576,11 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
@@ -499,11 +576,11 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
|
|
|
|
|
// If found in tree, fetch it
|
|
|
|
|
if (readmePath) { |
|
|
|
|
try { |
|
|
|
|
const fileData = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents/${readmePath}?ref=${repoData.default_branch}`).then(r => { |
|
|
|
|
if (!r.ok) throw new Error('Not found'); |
|
|
|
|
return r.json(); |
|
|
|
|
}); |
|
|
|
|
const fileResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents/${readmePath}?ref=${defaultBranch}`); |
|
|
|
|
if (!fileResponse.ok) throw new Error('Not found'); |
|
|
|
|
const fileData = await fileResponse.json(); |
|
|
|
|
if (fileData.content) { |
|
|
|
|
// Gitea returns base64 encoded content
|
|
|
|
|
const content = atob(fileData.content.replace(/\s/g, '')); |
|
|
|
|
const format = readmePath.toLowerCase().endsWith('.adoc') ? 'asciidoc' : 'markdown'; |
|
|
|
|
readme = { |
|
|
|
|
@ -512,17 +589,17 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
@@ -512,17 +589,17 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
|
|
|
|
|
format |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
// Failed to fetch from tree path
|
|
|
|
|
} catch (error) { |
|
|
|
|
console.warn('Failed to fetch README from tree path:', error); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
name: repoData.name, |
|
|
|
|
name: repoData.name || repoData.full_name?.split('/').pop() || repo, |
|
|
|
|
description: repoData.description, |
|
|
|
|
url: repoData.html_url, |
|
|
|
|
defaultBranch: repoData.default_branch, |
|
|
|
|
url: repoData.html_url || repoData.clone_url || `${baseUrl.replace('/api/v1', '')}/${owner}/${repo}`, |
|
|
|
|
defaultBranch: repoData.default_branch || defaultBranch, |
|
|
|
|
branches, |
|
|
|
|
commits, |
|
|
|
|
files, |
|
|
|
|
@ -552,6 +629,8 @@ export async function fetchGitRepo(url: string): Promise<GitRepoInfo | null> {
@@ -552,6 +629,8 @@ export async function fetchGitRepo(url: string): Promise<GitRepoInfo | null> {
|
|
|
|
|
case 'gitlab': |
|
|
|
|
return fetchFromGitLab(owner, repo, baseUrl); |
|
|
|
|
case 'gitea': |
|
|
|
|
case 'onedev': |
|
|
|
|
// OneDev uses a similar API structure to Gitea, so we can use the same handler
|
|
|
|
|
return fetchFromGitea(owner, repo, baseUrl); |
|
|
|
|
default: |
|
|
|
|
console.error('Unsupported platform:', platform); |
|
|
|
|
|