feat(web,ui): per-project git provider API base URL overrides

Per provider (github|gitlab|gitea) a project override stored in
~/.config/openchamber/projects/<projectId>.json under gitProviders wins
over the global settings.json value (precedence: project override > global
> built-in default). Server forge routes resolve the override per request
directory (worktree-aware via git-common-dir + containment + path fallback,
60s TTL cache); the override host is also accepted for remote parsing and
client detection. New GET/PUT /api/projects/:projectId/git-providers route;
client openchamberConfig preserves the server-owned gitProviders key;
Projects page gains a Git provider API base URLs section; detection store
hydrates per-project overrides (memory-only, server-authoritative).
This commit is contained in:
2026-08-17 09:57:54 +00:00
parent 697925ee0d
commit 66edc74fac
42 changed files with 1765 additions and 87 deletions
+20 -20
View File
@@ -210,9 +210,9 @@ export function registerGiteaRoutes(app, options = {}) {
return giteaLibraries;
};
const getClient = async () => {
const getClient = async (directory) => {
const { getGiteaClientOrNull } = await getGiteaLibraries();
return getGiteaClientOrNull();
return getGiteaClientOrNull(directory);
};
// Resolve which Gitea repo a request targets. A directory-local git remote
@@ -397,7 +397,7 @@ export function registerGiteaRoutes(app, options = {}) {
const effectivePage = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1;
const searchQuery = asString(req.query?.query);
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false, issues: [], page: effectivePage, hasMore: false });
}
@@ -449,7 +449,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'number is required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false, issue: null });
}
@@ -491,7 +491,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'number is required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false, comments: [] });
}
@@ -530,7 +530,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'directory, number, body are required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false });
}
@@ -582,7 +582,7 @@ export function registerGiteaRoutes(app, options = {}) {
? req.body.labels.filter((label) => typeof label === 'string' && label.length > 0)
: undefined;
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false });
}
@@ -632,7 +632,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'directory and number are required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false });
}
@@ -718,7 +718,7 @@ export function registerGiteaRoutes(app, options = {}) {
const searchQuery = asString(req.query?.query);
const sourceBranch = asString(req.query?.sourceBranch);
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false, prs: [], page: effectivePage, hasMore: false });
}
@@ -806,7 +806,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'number is required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false, pr: null, comments: [], files: [] });
}
@@ -917,7 +917,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'number is required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false, commits: [] });
}
@@ -973,7 +973,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'number is required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false, reviews: [] });
}
@@ -1026,7 +1026,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'number is required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false, statuses: [] });
}
@@ -1096,7 +1096,7 @@ export function registerGiteaRoutes(app, options = {}) {
? req.body.description
: undefined;
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false });
}
@@ -1152,7 +1152,7 @@ export function registerGiteaRoutes(app, options = {}) {
const title = asString(req.body?.title);
const description = typeof req.body?.description === 'string' ? req.body.description : undefined;
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false });
}
@@ -1214,7 +1214,7 @@ export function registerGiteaRoutes(app, options = {}) {
}
const method = ['merge', 'squash', 'rebase'].includes(req.body?.method) ? req.body.method : 'merge';
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false });
}
@@ -1270,7 +1270,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'directory, number, body are required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false });
}
@@ -1322,7 +1322,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'event must be APPROVED, REQUEST_CHANGES, or COMMENT' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false });
}
@@ -1438,7 +1438,7 @@ export function registerGiteaRoutes(app, options = {}) {
return res.status(400).json({ error: 'directory or owner/repo is required' });
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return res.json({ connected: false, labels: [] });
}
@@ -1488,7 +1488,7 @@ export function registerGiteaRoutes(app, options = {}) {
if (!directory && !requestedRepo) {
return { error: 'directory or owner/repo is required' };
}
const client = await getClient();
const client = await getClient(directory);
if (!client) {
return { client: null };
}