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)