diff --git a/server/api/changelog/md/[provider]/[owner]/[repo]/[...path].get.ts b/server/api/changelog/md/[provider]/[owner]/[repo]/[...path].get.ts index a1582b1414..1f7edb0e3c 100644 --- a/server/api/changelog/md/[provider]/[owner]/[repo]/[...path].get.ts +++ b/server/api/changelog/md/[provider]/[owner]/[repo]/[...path].get.ts @@ -2,7 +2,9 @@ import { resolveURL } from 'ufo' import * as v from 'valibot' import { getBaseFileUrl } from '~~/server/utils/changelog/baseFileUrl' import { + createBitbucketRepoInfo, createForgejoRepoInfo, + createGiteaRepoInfo, createGithubRepoInfo, createGitLabRepoInfo, createTangledInfo, @@ -56,6 +58,7 @@ export default defineCachedEventHandler( headers: { 'User-Agent': 'npmx.dev', }, + timeout: 15_000, }) const markdown = v.parse(v.string(), data) if (raw != undefined) { @@ -114,5 +117,9 @@ function getRepoInfo( return createGitLabRepoInfo(host ?? 'gitlab.com', owner, repo, path) case 'tangled': return createTangledInfo(owner, repo, path) + case 'gitea': + return createGiteaRepoInfo(host ?? 'gitea.com', owner, repo, path) + case 'bitbucket': + return createBitbucketRepoInfo(owner, repo, path) } } diff --git a/server/api/changelog/releases/[provider]/[owner]/[repo].get.ts b/server/api/changelog/releases/[provider]/[owner]/[repo].get.ts index 20e618072a..87b19ce128 100644 --- a/server/api/changelog/releases/[provider]/[owner]/[repo].get.ts +++ b/server/api/changelog/releases/[provider]/[owner]/[repo].get.ts @@ -9,11 +9,18 @@ import { GithubReleaseCollectionSchama, ForgejoReleaseCollectionSchema, GitlabReleaseCollectionSchema, + GiteaReleaseCollectionSchema, } from '~~/shared/schemas/changelog/release' import { changelogRenderer } from '~~/server/utils/changelog/markdown' -import { createForgejoRepoInfo, createGithubRepoInfo } from '~~/server/utils/changelog/mdRepoInfo' +import { + createForgejoRepoInfo, + createGiteaRepoInfo, + createGithubRepoInfo, +} from '~~/server/utils/changelog/mdRepoInfo' import { validateHostWithValibot } from '~~/server/utils/changelog/validateHost' +const TIMEOUT_TIME = 15_000 + export default defineCachedEventHandler( async event => { const provider = getRouterParam(event, 'provider') as ProviderId @@ -39,6 +46,8 @@ export default defineCachedEventHandler( return await getReleasesFromForgejo(owner, repo, host) case 'gitlab': return await getReleasesFromGitlab(owner, repo, host) + case 'gitea': + return await getReleasesFromGitea(owner, repo, host) default: throw createError({ @@ -79,6 +88,7 @@ async function getReleasesFromGithub(owner: string, repo: string) { 'Accept': '*/*', 'User-Agent': 'npmx.dev', }, + timeout: TIMEOUT_TIME, }) const { releases } = v.parse(GithubReleaseCollectionSchama, data) @@ -107,6 +117,7 @@ async function getReleasesFromForgejo(owner: string, repo: string, host: string) headers: { 'User-Agent': 'npmx.dev', }, + timeout: TIMEOUT_TIME, }) const releases = v.parse(ForgejoReleaseCollectionSchema, data) @@ -137,6 +148,7 @@ async function getReleasesFromGitlab(owner: string, repo: string, host: string) headers: { 'User-Agent': 'npmx.dev', }, + timeout: TIMEOUT_TIME, }) const releases = v.parse(GitlabReleaseCollectionSchema, data) @@ -158,3 +170,30 @@ async function getReleasesFromGitlab(owner: string, repo: string, host: string) } satisfies ReleaseData }) } + +async function getReleasesFromGitea(owner: string, repo: string, host: string) { + const data = await $fetch(`https://${host}/api/v1/repos/${owner}/${repo}/releases?draft=false`, { + headers: { + 'User-Agent': 'npmx.dev', + }, + timeout: TIMEOUT_TIME, + }) + const releases = v.parse(GiteaReleaseCollectionSchema, data) + + const render = await changelogRenderer(createGiteaRepoInfo(host, owner, repo)) + + return releases.map(r => { + const { html, toc } = render(r.body, r.id) + return { + id: r.id, + html: html?.replace(/(?)\n/g, '
') ?? null, + title: r.name || r.tag_name, + prerelease: r.prerelease, + toc, + link: r.html_url, + publishedAt: r.published_at, + draft: r.draft, + tag: r.tag_name, + } satisfies ReleaseData + }) +} diff --git a/server/api/changelog/releases/[provider]/[owner]/[repo]/raw/[tag].get.ts b/server/api/changelog/releases/[provider]/[owner]/[repo]/raw/[tag].get.ts index a09079a1a8..98bb2a25a9 100644 --- a/server/api/changelog/releases/[provider]/[owner]/[repo]/raw/[tag].get.ts +++ b/server/api/changelog/releases/[provider]/[owner]/[repo]/raw/[tag].get.ts @@ -8,6 +8,8 @@ import { } from '~~/shared/schemas/changelog/release' import { validateHostWithValibot } from '~~/server/utils/changelog/validateHost' +const TIMEOUT_TIME = 15_000 + export default defineCachedEventHandler( async event => { const provider = getRouterParam(event, 'provider') as ProviderId @@ -84,8 +86,9 @@ async function getMarkdownFromGithub(owner: string, repo: string, tag: string, e 'User-Agent': 'npmx.dev', }, ignoreResponseError: true, + timeout: TIMEOUT_TIME, }, - ) + ).catch(() => null) const parsed = v.safeParse(v.object({ body: v.string() }), responseGithub) @@ -101,6 +104,7 @@ async function getMarkdownFromGithub(owner: string, repo: string, tag: string, e 'Accept': '*/*', 'User-Agent': 'npmx.dev', }, + timeout: TIMEOUT_TIME, }) const { releases } = v.parse(GithubReleaseCollectionSchama, responseUngh) @@ -129,6 +133,7 @@ async function getMarkdownFromForgejo( headers: { 'User-Agent': 'npmx.dev', }, + timeout: TIMEOUT_TIME, }) const release = v.parse(ForgejoReleaseSchama, data) @@ -150,6 +155,7 @@ async function getMarkdownFromGitlab( headers: { 'User-Agent': 'npmx.dev', }, + timeout: TIMEOUT_TIME, }) const release = v.parse(GitlabReleaseSchame, data) diff --git a/server/utils/changelog/baseFileUrl.ts b/server/utils/changelog/baseFileUrl.ts index 7cda63169b..bd321ab81f 100644 --- a/server/utils/changelog/baseFileUrl.ts +++ b/server/utils/changelog/baseFileUrl.ts @@ -16,7 +16,14 @@ export function getBaseFileUrl(ref: RepoRef): RepoFileUrl | null { raw: `https://codeberg.org/${ref.owner}/${ref.repo}/raw/branch/HEAD`, } case 'forgejo': { - const host = ref.host + return { + blob: `https://${ref.host}/${ref.owner}/${ref.repo}/src/branch/HEAD`, + raw: `https://${ref.host}/${ref.owner}/${ref.repo}/raw/branch/HEAD`, + } + } + case 'gitea': { + // although similar to forgejo, we keep these seperate because they're seperately maintained projects + const host = ref.host ?? 'gitea.com' return { blob: `https://${host}/${ref.owner}/${ref.repo}/src/branch/HEAD`, raw: `https://${host}/${ref.owner}/${ref.repo}/raw/branch/HEAD`, @@ -35,6 +42,12 @@ export function getBaseFileUrl(ref: RepoRef): RepoFileUrl | null { raw: `https://tangled.org/${ref.owner}/${ref.repo}/raw/HEAD`, } } + case 'bitbucket': { + return { + blob: `https://bitbucket.org/${ref.owner}/${ref.repo}/src/HEAD`, + raw: `https://bitbucket.org/${ref.owner}/${ref.repo}/raw/HEAD`, + } + } } return null } diff --git a/server/utils/changelog/detectChangelog.ts b/server/utils/changelog/detectChangelog.ts index 6289a7aebd..1d04fd86df 100644 --- a/server/utils/changelog/detectChangelog.ts +++ b/server/utils/changelog/detectChangelog.ts @@ -8,6 +8,7 @@ import { GithubReleaseSchama, ForgejoReleaseSchama, GitlabReleaseSchame, + GiteaReleaseSchema, } from '~~/shared/schemas/changelog/release' import { resolveURL } from 'ufo' import * as v from 'valibot' @@ -66,6 +67,9 @@ async function checkReleases( case 'gitlab': { return checkLatestGitlabRelease(ref, directory) } + case 'gitea': { + return checkLatestGiteaRelease(ref, directory) + } } return [false, null] } @@ -340,3 +344,68 @@ async function checkLatestGitlabRelease( } return [false, null] } + +// gitea +async function checkLatestGiteaRelease( + ref: RepoRef, + directory?: string, +): Promise> { + try { + const host = ref.host ?? 'gitea.com' + + const response = await $fetch( + `https://${host}/api/v1/repos/${ref.owner}/${ref.repo}/releases/latest`, + { + headers: { + 'User-Agent': 'npmx.dev', + 'accept': 'application/json', + }, + }, + ) + + const release = v.parse(GiteaReleaseSchema, response) + + const matchedChangelog = release.body?.match(MD_REGEX)?.at(0) + + // /src/branch/ can be similar to /blob/ + if (!matchedChangelog || !matchedChangelog.includes('/src/branch/')) { + return [ + { + type: 'release', + link: `https://${host}/${ref.owner}/${ref.repo}/releases`, + provider: ref.provider, + repo: `${ref.owner}/${ref.repo}`, + host: ref.host, + }, + null, + ] + } + + const path = matchedChangelog.replace(/^.*\/src\/branch\/[^/]+\//i, '') + if ( + directory && + !( + path.startsWith(directory.endsWith('/') ? directory : `${directory}/`) || + ROOT_ONLY_REGEX.test(path) + ) + ) { + return [false, null] as const + } + return [ + { + provider: ref.provider, + type: 'md', + path, + repo: `${ref.owner}/${ref.repo}`, + link: matchedChangelog, + host: ref.host, + }, + null, + ] + } catch (e) { + if (e instanceof Error) { + return [null, e] + } + } + return [false, null] +} diff --git a/server/utils/changelog/markdown.ts b/server/utils/changelog/markdown.ts index e57d3f42e3..4edd42b82c 100644 --- a/server/utils/changelog/markdown.ts +++ b/server/utils/changelog/markdown.ts @@ -118,9 +118,9 @@ export interface MarkdownRepoInfo { /** the base url of repository commit */ commitBaseUrl: string /** base url for a repository issue */ - issueBaseUrl: string + issueBaseUrl?: string /** the text char that indicates an issue */ - issueChar: keyof typeof issuePrRegexes + issueChar?: keyof typeof issuePrRegexes /** base url for a repository pull/merge request */ prBaseUrl: string /** @@ -130,7 +130,7 @@ export interface MarkdownRepoInfo { x*/ prChar: keyof typeof issuePrRegexes /** base url for a repository compare */ - compareBaseUrl: string + compareBaseUrl?: string } function resolveUrl(url: string, repoInfo: MarkdownRepoInfo, toUserContentId: ToUserContentIdFn) { @@ -220,13 +220,13 @@ function resolveGitLinkText(href: string, label: string, repoInfo: MarkdownRepoI case href.startsWith(repoInfo.commitBaseUrl): { return lastSegment.slice(0, 7) // only show the first 6 letters/numbers of a commit } - case href.startsWith(repoInfo.issueBaseUrl): { + case !!repoInfo.issueBaseUrl && href.startsWith(repoInfo.issueBaseUrl): { return `${repoInfo.issueChar}${lastSegment}` } case href.startsWith(repoInfo.prBaseUrl): { return `${repoInfo.prChar}${lastSegment}` } - case href.startsWith(repoInfo.compareBaseUrl): { + case !!repoInfo.compareBaseUrl && href.startsWith(repoInfo.compareBaseUrl): { return lastSegment } // for account we don't resolve, this is something the git providers also don't do @@ -256,16 +256,19 @@ function createResolveGitTextToLinks(mdInfo: MarkdownRepoInfo): IOptions['textFi return `${match.slice(0, 7)}` }) - .replace(issuePrRegexes[mdInfo.issueChar], match => { - const id = match.replace(mdInfo.issueChar, '') - return `${match}` - }) // account .replace(accountRegex, match => { const acc = match.replace('@', '') return `${match}` }) + if (mdInfo.issueChar && mdInfo.issueBaseUrl) { + text = text.replace(issuePrRegexes[mdInfo.issueChar], match => { + const id = match.replace(mdInfo.issueChar!, '') + return `${match}` + }) + } + // pr/mr if (mdInfo.issueChar != mdInfo.prChar) { text = text.replace(issuePrRegexes[mdInfo.prChar], match => { diff --git a/server/utils/changelog/mdRepoInfo.ts b/server/utils/changelog/mdRepoInfo.ts index fca0f1dd74..2681d9bdaf 100644 --- a/server/utils/changelog/mdRepoInfo.ts +++ b/server/utils/changelog/mdRepoInfo.ts @@ -78,3 +78,43 @@ export function createTangledInfo(owner: string, repo: string, path?: string): M compareBaseUrl: `${hostBaseUrl}/${owner}/${repo}/compare`, } } + +// similar to forgejo, but they're seperate projects so keeping this also seperate +export function createGiteaRepoInfo( + host: string, + owner: string, + repo: string, + path?: string, +): MarkdownRepoInfo { + const hostBaseUrl = `https://${host}` + return { + hostBaseUrl, + blobBaseUrl: `${hostBaseUrl}/${owner}/${repo}/src/branch/HEAD`, + rawBaseUrl: `${hostBaseUrl}/${owner}/${repo}/raw/branch/HEAD`, + path, + commitBaseUrl: `${hostBaseUrl}/${owner}/${repo}/commit`, + issueChar: '#', + issueBaseUrl: `${hostBaseUrl}/${owner}/${repo}/issues`, + prChar: '#', + prBaseUrl: `${hostBaseUrl}/${owner}/${repo}/pulls`, + compareBaseUrl: `${hostBaseUrl}/${owner}/${repo}/compare`, + } +} + +export function createBitbucketRepoInfo( + owner: string, + repo: string, + path?: string, +): MarkdownRepoInfo { + const hostBaseUrl = 'https://bitbucket.org' + return { + hostBaseUrl, + blobBaseUrl: `${hostBaseUrl}/${owner}/${repo}/src/HEAD`, + rawBaseUrl: `${hostBaseUrl}/${owner}/${repo}/raw/HEAD`, + path, + commitBaseUrl: `${hostBaseUrl}/${owner}/${repo}/commits`, + prChar: '#', + prBaseUrl: `${hostBaseUrl}/${owner}/${repo}/pull-requests`, + // bitbucket uses jira instead of normal issues which can't be resolved, further the compare page also seems to be different + } +} diff --git a/shared/schemas/changelog/release.ts b/shared/schemas/changelog/release.ts index 0ac03ddd5c..8734395b3c 100644 --- a/shared/schemas/changelog/release.ts +++ b/shared/schemas/changelog/release.ts @@ -46,3 +46,16 @@ export const GitlabReleaseSchame = v.object({ }) export const GitlabReleaseCollectionSchema = v.array(GitlabReleaseSchame) + +export const GiteaReleaseSchema = v.object({ + id: v.number(), + tag_name: v.string(), + name: v.string(), + body: v.string(), + html_url: v.pipe(v.string(), v.url()), + draft: v.boolean(), + prerelease: v.boolean(), + published_at: v.pipe(v.string(), v.isoTimestamp()), +}) + +export const GiteaReleaseCollectionSchema = v.array(GiteaReleaseSchema)