fix(git-sync): drop the .git suffix from git http-backend PATH_INFO (smart-HTTP 404)

The /git smart-HTTP host 404'd EVERY fetch and push: PATH_INFO was built as
`/<spaceId>.git/<subpath>`, so `git http-backend` resolved the repo at
`<GIT_PROJECT_ROOT>/<spaceId>.git` — which does not exist. The vault is a NON-bare
working repo (the engine needs a working tree) at `<dataDir>/<spaceId>`, so the
CGI repo path must be `<spaceId>` (git http-backend serves the `.git` inside).
The URL's conventional `.git` suffix is already stripped to `spaceId` by
parseGitPath; re-appending it for PATH_INFO was the bug.

Found by standing up a full e2e stand (real Postgres/Redis + server + a real git
clone/push over the /git remote): clone and push both 404'd until this fix, after
which a clone → edit → push round-trips the change all the way into the Docmost
page.

Also extracts the CGI-env construction into a pure, exported `buildGitBackendCgiEnv`
and adds unit tests (the env build was previously untested — the gap this bug hid
in): a regression guard pinning PATH_INFO to `/<spaceId>/<subpath>` (no `.git`),
plus method/query/content-type/remote-user forwarding and the conditional
GIT_PROTOCOL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-24 01:52:17 +03:00
parent 12b201d231
commit 3c355de2be
2 changed files with 79 additions and 19 deletions

View File

@@ -101,6 +101,39 @@ export interface GitHttpBackendRequest {
* Node response. Errors before any output produce a 500. Credentials are never
* logged.
*/
/**
* Build the `git http-backend` CGI environment overlay for one request (the
* variables layered on top of `vaultGitEnv`'s cwd-isolated base). Pure so the
* PATH_INFO / REMOTE_USER / conditional GIT_PROTOCOL wiring is unit-testable
* without spawning git.
*
* PATH_INFO is the repo-relative CGI path. The vault is a NON-BARE working repo
* on disk at `<dataDir>/<spaceId>` (the engine needs a working tree), so the
* repo directory git http-backend must resolve is `<spaceId>` — NOT
* `<spaceId>.git`. The URL carries the conventional `.git` suffix (stripped by
* parseGitPath into `spaceId`); re-appending it here pointed the CGI at a
* non-existent `<dataDir>/<spaceId>.git` and every fetch/push 404'd.
*/
export function buildGitBackendCgiEnv(
parsed: GitHttpBackendRequest,
projectRoot: string,
): Record<string, string> {
const cgiEnv: Record<string, string> = {
GIT_PROJECT_ROOT: projectRoot,
GIT_HTTP_EXPORT_ALL: '1', // authz is done by us; no git-daemon-export-ok file
PATH_INFO: `/${parsed.spaceId}/${parsed.subpath}`,
REQUEST_METHOD: parsed.method,
QUERY_STRING: parsed.queryString,
CONTENT_TYPE: parsed.contentType,
REMOTE_USER: parsed.remoteUser,
};
// GIT_PROTOCOL is only set when the client sent the Git-Protocol header.
if (parsed.gitProtocol) {
cgiEnv.GIT_PROTOCOL = parsed.gitProtocol;
}
return cgiEnv;
}
@Injectable()
export class GitHttpBackendService {
private readonly logger = new Logger(GitHttpBackendService.name);
@@ -120,26 +153,11 @@ export class GitHttpBackendService {
rawRes: ServerResponse,
): Promise<void> {
const projectRoot = this.environmentService.getGitSyncDataDir();
// PATH_INFO is the repo-relative CGI path: /<spaceId>.git/<subpath>.
const pathInfo = `/${parsed.spaceId}.git/${parsed.subpath}`;
// Build the CGI env from the engine's cwd-isolated base (strips GIT_DIR /
// GIT_WORK_TREE), then layer the http-backend CGI variables. GIT_PROTOCOL is
// only set when the client sent the Git-Protocol header. PATH is preserved
// (vaultGitEnv already copies process.env, so PATH carries through).
const cgiEnv: Record<string, string> = {
GIT_PROJECT_ROOT: projectRoot,
GIT_HTTP_EXPORT_ALL: '1', // authz is done by us; no git-daemon-export-ok file
PATH_INFO: pathInfo,
REQUEST_METHOD: parsed.method,
QUERY_STRING: parsed.queryString,
CONTENT_TYPE: parsed.contentType,
REMOTE_USER: parsed.remoteUser,
};
if (parsed.gitProtocol) {
cgiEnv.GIT_PROTOCOL = parsed.gitProtocol;
}
const env = vaultGitEnv(cgiEnv);
// GIT_WORK_TREE), then layer the http-backend CGI variables. PATH is
// preserved (vaultGitEnv already copies process.env, so PATH carries
// through).
const env = vaultGitEnv(buildGitBackendCgiEnv(parsed, projectRoot));
return new Promise<void>((resolve) => {
let settled = false;