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).
7.7 KiB
7.7 KiB
Git Providers Configuration Module
Purpose
- This module owns the per-provider git hosting configuration (
gitProvidersin the user settings file): API base URLs and provider-detection hostnames for GitHub, GitLab, and Gitea. - It is the single source of truth for the effective provider defaults consumed by
packages/web/server/lib/{github,gitlab,gitea}and is validated end-to-end through the settings GET/PUT routes (thegitProviderskey round-trips viasanitizeSettingsUpdateinpackages/web/server/lib/opencode/settings-helpers.js). - Per-project API base URL overrides (
gitProvidersinprojects/<projectId>.json) extend the global settings; a project override wins per provider over the global value.
Entrypoints
packages/web/server/lib/git-providers/config.js: global settings helpers, exporting the helpers directly.packages/web/server/lib/git-providers/project-config.js: per-project git provider API base URL overrides (gitProvidersinprojects/<projectId>.json), including directory→projectId resolution.packages/web/server/lib/git-providers/routes.js:GET/PUT /api/projects/:projectId/git-providersAPI routes (wired viaregisterGitProviderRoutesinpackages/web/server/lib/opencode/feature-routes-runtime.js).
Public exports — config.js
GIT_PROVIDER_DEFAULTS:{ github: 'https://api.github.com', gitlab: 'https://gitlab.com', gitea: 'https://codeberg.org' }. Built-in defaults are not persisted; they are applied at read time by getters.GIT_PROVIDER_DEFAULT_DETECT_URLS:{ github: ['github.com'], gitlab: ['gitlab.com'], gitea: ['codeberg.org'] }. Built-in detection hostnames; remotes on these hosts classify as the provider with no configuration (mirrors the client-side built-ins inpackages/ui/src/lib/gitProvider.ts).normalizeBaseUrl(raw): normalize an API base URL (addhttps://when the scheme is missing, strip trailing slashes, preserve subpaths like/gitlab),nullfor empty/unparseable input.normalizeDetectionHost(raw): extract the bare lowercase hostname from any git remote/URL form (https://,ssh://, scp-likegit@host:path, IPv6); mirrorspackages/ui/src/lib/gitHost.tsparseGitHost.sanitizeGitProviders(payload): validate/normalize thegitProvidersshape — onlygithub|gitlab|giteakeys survive;apiBaseUrlvianormalizeBaseUrl,detectUrlsdeduped bare hostnames; empty/absent values dropped; returnsundefinedwhen nothing valid remains.readGitProvidersConfig(): read thegitProviderssection fromsettings.json(OPENCHAMBER_DATA_DIRenv override, else~/.config/openchamber); never throws, returns{}on missing/invalid data.getProviderApiBaseUrl(provider): configured value ->GIT_PROVIDER_DEFAULTS[provider]->null.getProviderDetectUrls(provider): effective detection hostnames — built-in default hosts plus configureddetectUrls, deduped (the built-ins always apply).githubWebOriginFromApiBase(apiBase): GitHub web origin from an API base —https://api.github.com->https://github.com; Enterprisehttps://host/api[/v3]->https://host(trailing/api//api/v3stripped, subpath prefixes kept); otherwise the URL origin; never throws, falls back tohttps://github.com.
Public exports — project-config.js
OPENCHAMBER_PROJECTS_DIR:path.join(OPENCHAMBER_DATA_DIR, 'projects')(sameOPENCHAMBER_DATA_DIRenv logic asconfig.js).sanitizeProjectGitProviders(payload): same provider allowlist/normalizeBaseUrlrules assanitizeGitProviders, but the per-project shape only carriesapiBaseUrl(detectUrlstolerated and stripped);undefinedwhen nothing valid remains.readProjectJson(projectId): raw JSON object fromprojects/<projectId>.json;{}on missing/malformed file,nullfor an invalid projectId; never throws.getProjectGitProviders(projectId): effective per-project overrides ({}when unset or invalid projectId).resolveProjectIdFromDirectory(directory): projectId for a directory — worktree-aware: the directory is first resolved to its main repo root viagit rev-parse --git-common-dir(handles linked worktrees created outside the project root, and a project rooted at the filesystem/), then the longest matching project path from the settings.jsonprojectslist that equals it or is a path-prefix wins; when git is unavailable or the directory is not a git repo, the directory's own exact/containment match applies; fallbackcreateProjectIdFromPath(directory);nullfor empty input. Results are cached per-directory for 60s (TTL cache, negative results included) so forge hot paths don't exec git / re-read settings.json per request._clearResolveProjectIdCache()is a test-only hook to drop the cache.getProjectProviderApiBaseUrl(provider, projectId): per-projectapiBaseUrloverride ornull.getEffectiveProviderApiBaseUrl(provider, directory): project override ->getProviderApiBaseUrl(provider)(global -> built-in default);nullonly when nothing resolves.saveProjectGitProviders(projectId, payload): persist the per-project overrides, preserving all other project JSON keys (atomic tmp-file + rename write); returns the savedgitProvidersobject (or{}); throws for an invalid projectId.
Routes
GET /api/projects/:projectId/git-providers→{ gitProviders: { github?: { apiBaseUrl }, ... } }.PUT /api/projects/:projectId/git-providerswith body{ gitProviders }→{ gitProviders }(saved);400on missing projectId or invalid body shape.
Settings shape
~/.config/openchamber/settings.json:
"gitProviders": {
"github": { "apiBaseUrl": "https://github.example.com/api/v3", "detectUrls": ["github.example.com"] },
"gitlab": { "apiBaseUrl": "https://gitlab.example.com", "detectUrls": [] },
"gitea": { "apiBaseUrl": "", "detectUrls": ["gitea.example.com"] }
}
apiBaseUrl: API base URL; the per-account baseUrl (gitlab/gitea accounts) still wins when set; this is the default/fallback plus connect-form prefill.detectUrls: SSH/HTTPS URLs normalized to bare hostnames for provider autodetection (client-side; the server only persists/validates them).- The whole
gitProviderskey is omitted when empty.
Per-project overrides
projects/<projectId>.json (under the same OPENCHAMBER_DATA_DIR root as settings.json):
{
"version": 1,
"projectNotes": "...",
"gitProviders": {
"github": { "apiBaseUrl": "https://project.github.example.com" },
"gitlab": { "apiBaseUrl": "https://project.gitlab.example.com" }
}
}
- Per-project
gitProviderscarryapiBaseUrlonly (nodetectUrls); unknown provider keys are dropped andapiBaseUrlis normalized with the same rules as the global settings. projects/<projectId>.jsonis shared with the scheduled-tasks/projectNotes config; reading and saving preserve all other keys (thegitProviderskey is omitted entirely when empty).- Precedence per provider: project override (
projects/<projectId>.json→getProjectProviderApiBaseUrl) > globalsettings.json(getProviderApiBaseUrl) > built-in default (GIT_PROVIDER_DEFAULTS).
Consumers
packages/web/server/lib/github/octokit.js,device-flow.js,routes.js,repo/index.js,pr-status.js,repo/fork-detection.js: GitHub Enterprise support (OctokitbaseUrl, device-flow web origin, remote parsing, fallback URLs).packages/web/server/lib/gitlab/auth.js,client.js,routes.js: effective default base URL.packages/web/server/lib/gitea/auth.js,routes.js: connect-form default / statusdefaultBaseUrl.packages/web/server/lib/opencode/settings-helpers.js:gitProviderspersistence whitelist.
Notes for contributors
- Readers must never throw:
readGitProvidersConfig,readProjectJson, andgithubWebOriginFromApiBasefail closed. - No new dependencies.