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
+15 -3
View File
@@ -1,4 +1,6 @@
import { getGiteaAuth } from './auth.js';
import { getProviderApiBaseUrl } from '../git-providers/config.js';
import { getEffectiveProviderApiBaseUrl } from '../git-providers/project-config.js';
// Per-request timeout for every Gitea call. Self-hosted instances can hang
// under load; bounding each request lets the caller fail fast and serve
@@ -306,11 +308,21 @@ export function createGiteaClient({ token, baseUrl }) {
};
}
/** Picks the current account (from auth.js) token + base URL, or null. */
export function getGiteaClientOrNull() {
/** Picks the current account (from auth.js) token + base URL, or null. A per-project override replaces the account's base URL for that project. */
export function getGiteaClientOrNull(directory) {
const auth = getGiteaAuth();
if (!auth?.accessToken || !auth?.baseUrl) {
return null;
}
return createGiteaClient({ token: auth.accessToken, baseUrl: auth.baseUrl });
let baseUrl = auth.baseUrl;
if (directory) {
const effectiveBaseUrl = getEffectiveProviderApiBaseUrl('gitea', directory);
// Only a per-project override replaces the account's base URL; without one
// the effective value is just the global default, which stored accounts
// (an explicit baseUrl is required) already outrank.
if (effectiveBaseUrl !== null && effectiveBaseUrl !== getProviderApiBaseUrl('gitea')) {
baseUrl = effectiveBaseUrl;
}
}
return createGiteaClient({ token: auth.accessToken, baseUrl });
}