Browse Source

add support for onedev, gitlab, and gitea

master
Silberengel 1 month ago
parent
commit
91a02f0aad
  1. 31
      src/lib/components/content/FileExplorer.svelte
  2. 181
      src/lib/services/content/git-repo-fetcher.ts

31
src/lib/components/content/FileExplorer.svelte

@ -95,20 +95,33 @@ @@ -95,20 +95,33 @@
const [, owner, repo] = match;
apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${file.path}?ref=${repoInfo.defaultBranch}`;
}
} else if (url.includes('gitlab.com')) {
// GitLab API: GET /projects/{id}/repository/files/{path}/raw
const match = url.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
} else if (url.includes('gitlab')) {
// GitLab API (both gitlab.com and self-hosted): GET /projects/{id}/repository/files/{path}/raw
const match = url.match(/(https?:\/\/[^/]*gitlab[^/]*)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
if (match) {
const [, owner, repo] = match;
const projectPath = encodeURIComponent(`${owner}/${repo}`);
apiUrl = `https://gitlab.com/api/v4/projects/${projectPath}/repository/files/${encodeURIComponent(file.path)}/raw?ref=${repoInfo.defaultBranch}`;
const [, baseHost, owner, repo] = match;
const cleanRepo = repo.replace(/\.git$/, '');
const projectPath = encodeURIComponent(`${owner}/${cleanRepo}`);
const apiBase = baseHost.includes('gitlab.com')
? 'https://gitlab.com/api/v4'
: `${baseHost}/api/v4`;
apiUrl = `${apiBase}/projects/${projectPath}/repository/files/${encodeURIComponent(file.path)}/raw?ref=${repoInfo.defaultBranch}`;
}
} else if (url.includes('onedev')) {
// OneDev API: similar to Gitea, uses /api/v1/repos/{owner}/{repo}/contents/{path}
const match = url.match(/(https?:\/\/[^/]*onedev[^/]*)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
if (match) {
const [, baseUrl, owner, repo] = match;
const cleanRepo = repo.replace(/\.git$/, '');
apiUrl = `${baseUrl}/api/v1/repos/${owner}/${cleanRepo}/contents/${file.path}?ref=${repoInfo.defaultBranch}`;
}
} else {
// Try Gitea pattern
const match = url.match(/(https?:\/\/[^/]+)\/([^/]+)\/([^/]+)/);
// Try Gitea/Forgejo pattern (handles .git suffix) - generic fallback
const match = url.match(/(https?:\/\/[^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
if (match) {
const [, baseUrl, owner, repo] = match;
apiUrl = `${baseUrl}/api/v1/repos/${owner}/${repo}/contents/${file.path}?ref=${repoInfo.defaultBranch}`;
const cleanRepo = repo.replace(/\.git$/, '');
apiUrl = `${baseUrl}/api/v1/repos/${owner}/${cleanRepo}/contents/${file.path}?ref=${repoInfo.defaultBranch}`;
}
}

181
src/lib/services/content/git-repo-fetcher.ts

@ -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);

Loading…
Cancel
Save