feat(git-sync): serve spaces over smart-HTTP (gitmost as a two-way git host)
Expose each git-sync-enabled space as a clonable/pushable git repo over HTTP, so `git clone https://<user>:<pass>@<host>/git/<spaceId>.git` works and external pushes flow back into Docmost pages — gitmost itself acts as the git host (no external GitHub/Gitea, no SSH). Transport: shell out to `git http-backend` (CGI; git is already in the runtime image) which implements the full smart-HTTP protocol (info/refs, upload-pack, receive-pack, protocol v2). A raw Fastify route `/git/*` (mounted at the root, outside the `/api` prefix) bridges the request/response to the CGI; passthrough content-type parsers for the git media types stream the raw body to stdin. Reuse the existing engine: clients push the vault's `main` branch, whose commits beyond `refs/docmost/last-pushed` the engine already reconciles into Docmost. - http/git-http.service.ts — auth (HTTP Basic -> AuthService.verifyUserCredentials), self-resolved workspace (DomainMiddleware does not run for this raw route), per-space gating (global + per-space gitSync flags, 404 hides existence), CASL authz (Read=fetch, Manage=push), dispatch. - http/git-http-backend.service.ts — spawn `git http-backend`, binary-safe CGI response parsing (Status/headers/body), stream to the socket. - http/git-http.helpers.ts — pure path parse, service->kind mapping, gate decision (unit-tested); rejects literal and percent-encoded path traversal. - orchestrator: extract reusable withSpaceLock (CAS-guarded lock heartbeat so a long push cannot let the lock expire mid-cycle) and add ingestExternalPush (receive-pack + Docmost cycle under one lock; 503 on contention). - vault-registry: ensureServable() — ensureRepo + idempotent receive.denyCurrentBranch =updateInstead / denyNonFastForwards / http.receivepack / http.uploadpack. - env: GIT_SYNC_HTTP_ENABLED (defaults to GIT_SYNC_ENABLED) + validation. - main.ts: register the /git/* route and the git content-type parsers. Tests: pure helpers, CGI parsing, and the GitHttpService handler (auth/gate/authz + workspace resolution). Server tsc + git-sync/env suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
claude code agent 227
parent
6e8d24175e
commit
6debcb354a
@@ -15,6 +15,7 @@ import { InternalLogFilter } from './common/logger/internal-log-filter';
|
||||
import { EnvironmentService } from './integrations/environment/environment.service';
|
||||
import { resolveFrameHeader } from './common/helpers';
|
||||
import { resolveTrustProxy } from './integrations/environment/trust-proxy.util';
|
||||
import { GitHttpService } from './integrations/git-sync/http/git-http.service';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
@@ -99,6 +100,23 @@ async function bootstrap() {
|
||||
},
|
||||
);
|
||||
|
||||
// git smart-HTTP POST bodies use these media types. Register PASSTHROUGH
|
||||
// content-type parsers so Fastify does NOT buffer/parse them (it would
|
||||
// otherwise reject the unknown type with 415); the /git handler streams the
|
||||
// raw Node request (request.raw) to `git http-backend` stdin instead. A
|
||||
// passthrough parser also bypasses the bodyLimit, so large pushes are not
|
||||
// truncated (the bytes are never buffered by Fastify).
|
||||
app
|
||||
.getHttpAdapter()
|
||||
.getInstance()
|
||||
.addContentTypeParser(
|
||||
[
|
||||
'application/x-git-upload-pack-request',
|
||||
'application/x-git-receive-pack-request',
|
||||
],
|
||||
(_req, payload, done) => done(null, payload),
|
||||
);
|
||||
|
||||
app
|
||||
.getHttpAdapter()
|
||||
.getInstance()
|
||||
@@ -146,6 +164,25 @@ async function bootstrap() {
|
||||
app.useGlobalInterceptors(new TransformHttpResponseInterceptor(reflector));
|
||||
app.enableShutdownHooks();
|
||||
|
||||
// git smart-HTTP host (the /git/<spaceId>.git/... subtree). Registered as a
|
||||
// RAW Fastify route — NOT a Nest controller under the global '/api' prefix —
|
||||
// so it lives at the ROOT and a single wildcard reliably captures the whole
|
||||
// multi-segment subtree (avoiding the path-to-regexp v8 wildcard / global-
|
||||
// prefix-exclude ambiguity in NestJS v11). The handler is resolved from the
|
||||
// Nest container so all auth/authz/gating still runs. NOTE: Nest middleware
|
||||
// (DomainMiddleware) does NOT run for this raw root route — it is bound to the
|
||||
// Nest router under the global '/api' prefix — so request.raw.workspaceId is
|
||||
// NOT populated here; GitHttpService resolves the workspace itself (mirroring
|
||||
// DomainMiddleware). The Fastify wildcard '/git/*' captures the multi-segment
|
||||
// subpath; the handler re-parses req.url itself.
|
||||
const gitHttpService = app.get(GitHttpService);
|
||||
app
|
||||
.getHttpAdapter()
|
||||
.getInstance()
|
||||
.all('/git/*', async (request, reply) => {
|
||||
await gitHttpService.handle(request as any, reply as any);
|
||||
});
|
||||
|
||||
const logger = new Logger('NestApplication');
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
|
||||
Reference in New Issue
Block a user