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
2ae6f8841f
commit
07b3962811
@@ -1,8 +1,12 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { VaultGit } from '@docmost/git-sync';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { VaultGit, vaultGitEnv } from '@docmost/git-sync';
|
||||
import { EnvironmentService } from '../../environment/environment.service';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Resolves the on-disk vault location per space and owns the (lazily created,
|
||||
* cached) `VaultGit` instance for each one (plan §3/§5).
|
||||
@@ -41,4 +45,49 @@ export class VaultRegistryService {
|
||||
this.vaults.set(spaceId, vault);
|
||||
return vault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a space's vault repo servable over smart-HTTP (the /git host). Ensures
|
||||
* the repo exists (engine `ensureRepo`: `git init -b main` + initial commit +
|
||||
* branches; idempotent), then sets the LOCAL git config a `git http-backend`
|
||||
* push needs:
|
||||
*
|
||||
* - receive.denyCurrentBranch=updateInstead — a push to the checked-out
|
||||
* `main` updates the working tree too (the engine's human-facing branch).
|
||||
* Requires a clean tree, which is guaranteed between cycles / under the
|
||||
* orchestrator lock that wraps an external push.
|
||||
* - receive.denyNonFastForwards=true — block force-push so a client cannot
|
||||
* rewrite the engine's history on `main`.
|
||||
* - http.receivepack=true / http.uploadpack=true — explicitly allow the
|
||||
* receive/upload services over HTTP.
|
||||
*
|
||||
* All four are set idempotently (plain `git config` overwrites the local
|
||||
* value). Returns the absolute vault path. Idempotent and safe to call before
|
||||
* every request.
|
||||
*/
|
||||
async ensureServable(spaceId: string): Promise<string> {
|
||||
const vault = await this.getVault(spaceId);
|
||||
const path = this.vaultPath(spaceId);
|
||||
|
||||
// ensureRepo also verifies git is available on its first git call; it does
|
||||
// `git init -b main` + an initial commit + the engine branches. Idempotent.
|
||||
await vault.ensureRepo();
|
||||
|
||||
const configs: Array<[string, string]> = [
|
||||
['receive.denyCurrentBranch', 'updateInstead'],
|
||||
['receive.denyNonFastForwards', 'true'],
|
||||
['http.receivepack', 'true'],
|
||||
['http.uploadpack', 'true'],
|
||||
];
|
||||
for (const [key, value] of configs) {
|
||||
await execFileAsync('git', ['config', key, value], {
|
||||
cwd: path,
|
||||
// Use the engine's cwd-isolated env (strips GIT_DIR / GIT_WORK_TREE) so
|
||||
// the config is written to THIS vault's local config, nothing else.
|
||||
env: vaultGitEnv(),
|
||||
});
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user