From b85f70dc9527676a542b6caa6dc7ab9a87b2547b Mon Sep 17 00:00:00 2001 From: plenarius Date: Mon, 20 Apr 2026 23:05:38 -0400 Subject: [PATCH] refactor: replace GitHub API with Gitea-compatible git provider service --- package-lock.json | 190 ------------------ packages/backend/package.json | 1 - packages/backend/src/config/repos.ts | 8 +- packages/backend/src/routes/github.ts | 12 +- .../src/services/git-provider.service.ts | 98 +++++++++ packages/backend/src/services/git.service.ts | 44 ++-- .../backend/src/services/github.service.ts | 70 ------- 7 files changed, 140 insertions(+), 283 deletions(-) create mode 100644 packages/backend/src/services/git-provider.service.ts delete mode 100644 packages/backend/src/services/github.service.ts diff --git a/package-lock.json b/package-lock.json index 3a45c48..5b63c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -898,161 +898,6 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/core": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", - "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.3", - "@octokit/request": "^10.0.6", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/endpoint": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", - "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/graphql": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", - "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", - "license": "MIT", - "dependencies": { - "@octokit/request": "^10.0.6", - "@octokit/types": "^16.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", - "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", - "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-request-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", - "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", - "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/request": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", - "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^11.0.3", - "@octokit/request-error": "^7.0.2", - "@octokit/types": "^16.0.0", - "fast-content-type-parse": "^3.0.0", - "json-with-bigint": "^3.5.3", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/request-error": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", - "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/rest": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", - "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", - "license": "MIT", - "dependencies": { - "@octokit/core": "^7.0.6", - "@octokit/plugin-paginate-rest": "^14.0.0", - "@octokit/plugin-request-log": "^6.0.0", - "@octokit/plugin-rest-endpoint-methods": "^17.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/types": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", - "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^27.0.0" - } - }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2041,12 +1886,6 @@ "tweetnacl": "^0.14.3" } }, - "node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", - "license": "Apache-2.0" - }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -2733,22 +2572,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", - "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3057,12 +2880,6 @@ "node": ">=6" } }, - "node_modules/json-with-bigint": { - "version": "3.5.8", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", - "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", - "license": "MIT" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4181,12 +3998,6 @@ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "license": "MIT" }, - "node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", - "license": "ISC" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5065,7 +4876,6 @@ "name": "@layonara-forge/backend", "version": "0.0.1", "dependencies": { - "@octokit/rest": "^22.0.1", "chokidar": "^4.0.0", "cors": "^2.8.5", "dockerode": "^4.0.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index 39ed040..ee367d2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -10,7 +10,6 @@ "test": "vitest" }, "dependencies": { - "@octokit/rest": "^22.0.1", "chokidar": "^4.0.0", "cors": "^2.8.5", "dockerode": "^4.0.0", diff --git a/packages/backend/src/config/repos.ts b/packages/backend/src/config/repos.ts index 5aa4e93..cbe2404 100644 --- a/packages/backend/src/config/repos.ts +++ b/packages/backend/src/config/repos.ts @@ -1,7 +1,9 @@ +export const GIT_PROVIDER_URL = process.env.GIT_PROVIDER_URL || "https://gitea.layonara.com"; + export const REPOS = [ - { name: "nwn-module", upstream: "layonara/nwn-module", branch: "ee" }, - { name: "nwn-haks", upstream: "layonara/nwn-haks", branch: "64bit" }, - { name: "unified", upstream: "plenarius/unified", branch: "master" }, + { name: "nwn-module", upstream: "layonara/nwn-module", branch: "ee", provider: "gitea" }, + { name: "nwn-haks", upstream: "layonara/nwn-haks", branch: "64bit", provider: "gitea" }, + { name: "unified", upstream: "plenarius/unified", branch: "master", provider: "github" }, ] as const; export type RepoName = (typeof REPOS)[number]["name"]; diff --git a/packages/backend/src/routes/github.ts b/packages/backend/src/routes/github.ts index afedba8..1cb657d 100644 --- a/packages/backend/src/routes/github.ts +++ b/packages/backend/src/routes/github.ts @@ -2,19 +2,19 @@ import { Router } from "express"; import { readConfig, writeConfig } from "../services/workspace.service.js"; import { REPOS } from "../config/repos.js"; import { - validatePat, + validateToken, forkRepo, listUserForks, createPullRequest, listPullRequests, -} from "../services/github.service.js"; +} from "../services/git-provider.service.js"; const router = Router(); async function getPat(): Promise { const config = await readConfig(); const pat = config.githubPat; - if (!pat) throw new Error("GitHub PAT not configured"); + if (!pat) throw new Error("Git provider token not configured"); return pat; } @@ -25,7 +25,7 @@ router.post("/validate-pat", async (req, res) => { res.status(400).json({ error: "PAT is required" }); return; } - const result = await validatePat(pat); + const result = await validateToken(pat); const config = await readConfig(); await writeConfig({ ...config, githubPat: pat }); @@ -76,8 +76,8 @@ router.post("/pr", async (req, res) => { return; } - const { login } = await import("../services/github.service.js").then((m) => - m.validatePat(pat), + const { login } = await import("../services/git-provider.service.js").then((m) => + m.validateToken(pat), ); const result = await createPullRequest(pat, { diff --git a/packages/backend/src/services/git-provider.service.ts b/packages/backend/src/services/git-provider.service.ts new file mode 100644 index 0000000..42e3287 --- /dev/null +++ b/packages/backend/src/services/git-provider.service.ts @@ -0,0 +1,98 @@ +import { REPOS, GIT_PROVIDER_URL } from "../config/repos.js"; + +async function giteaFetch(path: string, token: string, options: RequestInit = {}): Promise { + return fetch(`${GIT_PROVIDER_URL}/api/v1${path}`, { + ...options, + headers: { + "Authorization": `token ${token}`, + "Content-Type": "application/json", + ...options.headers, + }, + }); +} + +export async function validateToken(token: string) { + const res = await giteaFetch("/user", token); + if (!res.ok) { + throw new Error(`Token validation failed: ${res.status} ${res.statusText}`); + } + const data = await res.json(); + return { login: data.login, email: data.email }; +} + +export async function forkRepo(token: string, owner: string, repo: string) { + const res = await giteaFetch(`/repos/${owner}/${repo}/forks`, token, { + method: "POST", + body: JSON.stringify({}), + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.message || `Fork failed: ${res.status} ${res.statusText}`); + } + const data = await res.json(); + return { fullName: data.full_name, cloneUrl: data.clone_url }; +} + +export async function listUserForks(token: string) { + const { login } = await validateToken(token); + + const results: Array<{ repo: string; forked: boolean; fullName?: string }> = []; + + for (const repo of REPOS) { + if (repo.provider !== "gitea") { + results.push({ repo: repo.name, forked: false }); + continue; + } + try { + const res = await giteaFetch(`/repos/${login}/${repo.name}`, token); + if (res.ok) { + const data = await res.json(); + results.push({ repo: repo.name, forked: true, fullName: data.full_name }); + } else { + results.push({ repo: repo.name, forked: false }); + } + } catch { + results.push({ repo: repo.name, forked: false }); + } + } + + return results; +} + +export async function createPullRequest( + token: string, + opts: { upstream: string; repo: string; title: string; body: string; head: string; base: string }, +) { + const [owner, repo] = opts.upstream.split("/"); + const res = await giteaFetch(`/repos/${owner}/${repo}/pulls`, token, { + method: "POST", + body: JSON.stringify({ + title: opts.title, + body: opts.body, + head: opts.head, + base: opts.base, + }), + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.message || `PR creation failed: ${res.status} ${res.statusText}`); + } + const data = await res.json(); + return { number: data.number, url: data.html_url }; +} + +export async function listPullRequests(token: string, owner: string, repo: string) { + const res = await giteaFetch(`/repos/${owner}/${repo}/pulls?state=open`, token); + if (!res.ok) { + throw new Error(`Failed to list PRs: ${res.status} ${res.statusText}`); + } + const data = await res.json(); + return data.map((pr: Record) => ({ + number: pr.number, + title: pr.title, + state: pr.state, + url: pr.html_url, + user: (pr.user as Record)?.login || "", + createdAt: pr.created_at, + })); +} diff --git a/packages/backend/src/services/git.service.ts b/packages/backend/src/services/git.service.ts index 0f3e262..c6acebf 100644 --- a/packages/backend/src/services/git.service.ts +++ b/packages/backend/src/services/git.service.ts @@ -1,6 +1,6 @@ import simpleGit, { SimpleGit } from "simple-git"; import fs from "fs/promises"; -import { REPOS, type RepoName } from "../config/repos.js"; +import { REPOS, GIT_PROVIDER_URL, type RepoName } from "../config/repos.js"; import { getRepoPath, readConfig } from "./workspace.service.js"; import { broadcast } from "./ws.service.js"; @@ -111,19 +111,29 @@ export async function getDiff(repoPath: string, file?: string) { return g.diff(["HEAD"]); } +function getCloneUrl(token: string, user: string, repoName: string, provider: string): string { + const host = provider === "github" ? "github.com" : new URL(GIT_PROVIDER_URL).host; + return `https://${token}@${host}/${user}/${repoName}.git`; +} + +function getUpstreamUrl(owner: string, repo: string, provider: string, token?: string): string { + if (provider === "github") { + return `https://github.com/${owner}/${repo}.git`; + } + const host = new URL(GIT_PROVIDER_URL).host; + if (token) { + return `https://${token}@${host}/${owner}/${repo}.git`; + } + return `https://${host}/${owner}/${repo}.git`; +} + export async function setupClone(repoName: RepoName) { const config = await readConfig(); const pat = config.githubPat; - if (!pat) throw new Error("GitHub PAT not configured"); + if (!pat) throw new Error("Git provider token not configured"); const repoDef = getRepoDef(repoName); const [upOwner, upRepo] = repoDef.upstream.split("/"); - - const { validatePat } = await import("./github.service.js"); - const { login } = await validatePat(pat); - - const forkUrl = `https://${pat}@github.com/${login}/${upRepo}.git`; - const upstreamUrl = `https://github.com/${upOwner}/${upRepo}.git`; const targetDir = getRepoPath(repoName); try { @@ -133,11 +143,19 @@ export async function setupClone(repoName: RepoName) { if (err instanceof Error && err.message.startsWith("Repo directory")) throw err; } - await cloneRepo(forkUrl, targetDir, { branch: repoDef.branch, depth: 1 }); - - const g = git(targetDir); - await g.addRemote("upstream", upstreamUrl); - await g.fetch("upstream"); + if (repoDef.provider === "github") { + const upstreamUrl = getUpstreamUrl(upOwner, upRepo, "github"); + await cloneRepo(upstreamUrl, targetDir, { branch: repoDef.branch, depth: 1 }); + } else { + const { validateToken } = await import("./git-provider.service.js"); + const { login } = await validateToken(pat as string); + const forkUrl = getCloneUrl(pat as string, login, upRepo, repoDef.provider); + const upstreamUrl = getUpstreamUrl(upOwner, upRepo, repoDef.provider, pat as string); + await cloneRepo(forkUrl, targetDir, { branch: repoDef.branch, depth: 1 }); + const g = git(targetDir); + await g.addRemote("upstream", upstreamUrl); + await g.fetch("upstream"); + } return { path: targetDir, branch: repoDef.branch }; } diff --git a/packages/backend/src/services/github.service.ts b/packages/backend/src/services/github.service.ts deleted file mode 100644 index f2eadc2..0000000 --- a/packages/backend/src/services/github.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Octokit } from "@octokit/rest"; -import { REPOS } from "../config/repos.js"; - -export async function validatePat(pat: string) { - const octokit = new Octokit({ auth: pat }); - const { data, headers } = await octokit.rest.users.getAuthenticated(); - const scopes = (headers["x-oauth-scopes"] || "") - .split(",") - .map((s: string) => s.trim()) - .filter(Boolean); - return { login: data.login, scopes }; -} - -export async function forkRepo(pat: string, owner: string, repo: string) { - const octokit = new Octokit({ auth: pat }); - const { data } = await octokit.rest.repos.createFork({ owner, repo }); - return { fullName: data.full_name, cloneUrl: data.clone_url }; -} - -export async function listUserForks(pat: string) { - const octokit = new Octokit({ auth: pat }); - const { data: user } = await octokit.rest.users.getAuthenticated(); - const login = user.login; - - const results: Array<{ repo: string; forked: boolean; fullName?: string }> = []; - - for (const repo of REPOS) { - try { - const { data } = await octokit.rest.repos.get({ - owner: login, - repo: repo.name, - }); - results.push({ repo: repo.name, forked: true, fullName: data.full_name }); - } catch { - results.push({ repo: repo.name, forked: false }); - } - } - - return results; -} - -export async function createPullRequest( - pat: string, - opts: { upstream: string; repo: string; title: string; body: string; head: string; base: string }, -) { - const octokit = new Octokit({ auth: pat }); - const [owner, repo] = opts.upstream.split("/"); - const { data } = await octokit.rest.pulls.create({ - owner, - repo, - title: opts.title, - body: opts.body, - head: opts.head, - base: opts.base, - }); - return { number: data.number, url: data.html_url }; -} - -export async function listPullRequests(pat: string, owner: string, repo: string) { - const octokit = new Octokit({ auth: pat }); - const { data } = await octokit.rest.pulls.list({ owner, repo, state: "open" }); - return data.map((pr) => ({ - number: pr.number, - title: pr.title, - state: pr.state, - url: pr.html_url, - user: pr.user?.login || "", - createdAt: pr.created_at, - })); -}