refactor: replace GitHub API with Gitea-compatible git provider service
This commit is contained in:
@@ -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"];
|
||||
|
||||
@@ -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<string> {
|
||||
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, {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { REPOS, GIT_PROVIDER_URL } from "../config/repos.js";
|
||||
|
||||
async function giteaFetch(path: string, token: string, options: RequestInit = {}): Promise<Response> {
|
||||
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<string, unknown>) => ({
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: pr.state,
|
||||
url: pr.html_url,
|
||||
user: (pr.user as Record<string, unknown>)?.login || "",
|
||||
createdAt: pr.created_at,
|
||||
}));
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user