@foss.global/gitmanager
Headless Git manager for the foss.global Git platform.
This package follows the push.rocks Rust-backed package pattern: TypeScript owns the public package API and lifecycle, while Rust owns the Git/storage/protocol engine. TypeScript controls the Rust binary through @push.rocks/smartrust, and builds use @git.zone/tsrust.
The current implementation provides lifecycle IPC, local bare repository and mirror management, bounded read APIs, durable repository bundle transfer, public and trusted-ingress private clone/fetch, trusted-ingress receive-pack, bounded raw Git services for SSH ingress, and revision-fenced protection for exact branch refs. User identity, permissions, SSH/TLS termination, durable protected-ref policy storage, and durable post-push repository storage remain the embedding service's responsibility.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm add @foss.global/gitmanager
Usage
import { GitManager } from '@foss.global/gitmanager';
const gitManager = new GitManager({
dataDir: '/var/lib/foss-global/gitmanager',
});
await gitManager.start();
const status = await gitManager.getStatus();
console.log(status.running, status.dataDir);
await gitManager.importMirror({
org: 'foss.global',
repo: 'interfaces',
remoteUrl: 'ssh://git@code.foss.global:29419/foss.global/interfaces.git',
});
const refs = await gitManager.listRefs({ org: 'foss.global', repo: 'interfaces' });
const tree = await gitManager.listTree({ org: 'foss.global', repo: 'interfaces', ref: 'main' });
const readme = await gitManager.readFile({
org: 'foss.global',
repo: 'interfaces',
ref: 'main',
path: 'readme.md',
});
await gitManager.stop();
Durable repository bundles
GitManager can export a repository as a full Git bundle and atomically restore it under a new managed repository identity. Bundle bytes never pass through JSON IPC: Rust writes or reads a temporary file inside the dedicated transferDir, and the caller streams that file to or from durable object storage.
const gitManager = new GitManager({
dataDir: '/run/foss-global/gitmanager',
transferDir: '/run/foss-global/git-transfers',
maxBundleBytes: 8 * 1024 * 1024 * 1024,
});
const bundle = await gitManager.createRepositoryBundle({
org: 'foss.global',
repo: 'interfaces',
});
// Stream bundle.bundlePath to durable object storage, then remove the temporary file.
await gitManager.restoreRepositoryBundle({
org: 'restored',
repo: 'interfaces',
bundlePath: '/run/foss-global/git-transfers/downloaded.bundle',
defaultBranch: 'main',
});
transferDir defaults to <dataDir>/transfers; both paths must be absolute. maxBundleBytes defaults to 8 GiB and is enforced after export and before restore. Restore accepts only a direct, non-empty .bundle file inside transferDir, requires the declared default branch, runs strict full git fsck, and moves the restored bare repository into place atomically. Empty repositories have no Git refs and therefore cannot produce a bundle; persist their catalog metadata and recreate them with initBareRepository().
Exact-commit source archives default to a 128 MiB compressed limit, a 64 GiB extracted regular-file limit, and 1,000,000 filesystem entries. Configure these with maxArchiveBytes, maxArchiveExtractedBytes, and maxArchiveEntries; their hard ceilings are 16 GiB, 64 GiB, and 1,000,000 respectively. CI source consumers apply their negotiated compressed limit, which is at most 8 GiB in runner protocol v2. Before publication, GitManager validates the exact generated gzip/tar stream against the returned extractedSizeBytes and entryCount, including paths, entry types, links, PAX metadata, checksums, padding, and archive terminators.
The transfer directory is disposable staging, not persistence. GitManager holds exclusive ownership of it while running so startup can safely remove interrupted archive temporary files. The caller owns upload/download completion and removal of finished transfer files. Durable repository bytes belong in managed object storage.
Git Smart HTTP
Smart HTTP is disabled unless a smartHttp options object is provided. The safe defaults bind to loopback on an operating-system-assigned port and expose no repositories:
const gitManager = new GitManager({
dataDir: '/var/lib/foss-global/gitmanager',
smartHttp: {
allowedRepositories: [
{ org: 'foss.global', repo: 'interfaces' },
],
privateRead: {
trustedIngressToken: process.env.GITMANAGER_INGRESS_TOKEN!,
},
receivePack: {
trustedIngressToken: process.env.GITMANAGER_INGRESS_TOKEN!,
},
},
});
await gitManager.start();
const status = await gitManager.getStatus();
console.log(status.smartHttp?.baseUrl);
The server implements git-upload-pack for clone and fetch with Git protocol versions 0, 1, and 2. Missing and inaccessible read repositories both return the same not-found response. Configuring privateRead lets an authenticated loopback ingress grant repository-scoped access by injecting the exact X-GitManager-Ingress-Token value after removing any client-supplied copy. Direct private requests still receive the same not-found response.
Configuring receivePack enables streamed push only for requests carrying the trusted ingress token. Private reads and receive-pack require a loopback bind even when allowUnsafeNonLoopback is set. The trusted ingress must authorize the user and repository; for pushes it must also serialize writes, persist the updated repository, and withhold success if persistence fails.
The transport does not terminate TLS or implement user identity. Keep it behind a bounded ingress. Read-only binding to a non-loopback IP is rejected unless allowUnsafeNonLoopback: true is set explicitly.
Smart HTTP option defaults and accepted ranges:
All numeric options must be integers.
enabled: enabled when thesmartHttpobject is present unless explicitlyfalse.bindAddress:127.0.0.1; must be an IP address.port:0, allowing the operating system to select an available port; accepted range 0–65,535.allowUnsafeNonLoopback:false.allowAllRepositories:false.allowedRepositories:[], with at most 100,000 entries.maxConcurrentTransfers:32; accepted range 1–1,024.maxRequestBodyBytes: 16 MiB; accepted range 1 KiB–64 MiB.maxAdvertisementBytes: 4 MiB; accepted range 1 KiB–64 MiB.maxResponseBytes: 4 GiB; accepted range 1 MiB–16 TiB.processTimeoutMs: 1,800,000; accepted range 1,000–86,400,000.idleTimeoutSeconds:300; accepted range 1–3,600.gracefulShutdownMs:10,000; accepted range 100–60,000.privateRead.trustedIngressToken: required when trusted private reads are enabled; 32–512 non-whitespace characters.receivePack.trustedIngressToken: required when receive-pack is enabled; 32–512 non-whitespace characters.receivePack.maxRequestBodyBytes: 4 GiB; accepted range 1 KiB–16 TiB. Request bodies are streamed and counted.
While the service is running, access can be replaced atomically:
await gitManager.setSmartHttpRepositoryAccess({
allowAllRepositories: false,
allowedRepositories: [
{ org: 'foss.global', repo: 'interfaces' },
{ org: 'foss.global', repo: 'gitmanager' },
],
});
setSmartHttpRepositoryAccess() replaces the complete allow-all/allowlist state and resets per-repository visibility revision watermarks. For ordered visibility events, keep allowAllRepositories disabled and update one repository at a time:
await gitManager.setSmartHttpRepositoryVisibility({
org: 'foss.global',
repo: 'gitmanager',
public: true,
revision: 42,
});
Use monotonic, non-negative safe integers for revision. Stale revisions are ignored, equal revisions are idempotent when their visibility matches, conflicting equal revisions fail, and newer revisions replace the previous visibility state.
Protected refs
Protected refs are configured per repository as one complete, revisioned rule set:
const result = await gitManager.setSmartHttpRepositoryProtectedRefs({
org: 'foss.global',
repo: 'gitmanager',
revision: 1,
rules: [
{
refName: 'refs/heads/main',
allowCreates: false,
allowDeletes: false,
allowForcePushes: false,
},
],
});
console.log(result.status, result.effectiveRevision, result.rulesDigest);
Rules accept canonical, exact refs/heads/... names only; patterns, tags, roles, and bypass identities are not supported. All three permission booleans are required. Unlisted refs are unaffected. GitManager accepts at most 64 unique rules and canonicalizes their order.
Protected creates, deletes, and updates require valid commit objects. When allowForcePushes is false, an existing protected branch can move only through a fast-forward update.
Every start begins with no admitted policy for any repository, so receive-pack fails closed until the embedding service explicitly initializes that repository. Revision 0 is reserved for the empty policy and must use rules: []; the first non-empty policy must use revision 1 or higher. Clearing protection requires a newer revision with rules: [].
The first explicit policy after a start and every higher revision return applied. An equal revision with identical canonical rules returns idempotent. A lower revision returns stale with the effective revision and digest, while conflicting rules at an equal revision fail. Every successful result includes the effective revision and SHA-256 digest of the canonical rules.
Protected-ref state is process-local by design. The embedding service must persist the desired revision and rules, replay them after every start(), verify that the returned effective revision and digest match the desired policy, and admit no pushes before that verification succeeds. Policy mutation and receive-pack admission for the same repository must be serialized. Each receive-pack invocation is bound to one immutable policy snapshot.
Receive-pack uses only GitManager's private, process-specific hook directory; repository-configured hooks are not executed. The managed pre-receive hook rejects the complete push when policy bootstrap, decoding, schema validation, repository selection, object-format validation, commit or ancestry checks, or timeout handling fails.
When enabled, getStatus().smartHttp reports { running, bindAddress, port, baseUrl }. The server also exposes GET /healthz for a local ingress health check.
Raw Git services for SSH ingress
openRepositoryService() starts a bounded local git upload-pack or protected git receive-pack process for an already authenticated ingress. Rust validates the repository selector, canonical path, Git protocol version, receive-pack availability, and immutable protected-ref policy snapshot before the TypeScript facade starts the process. Pack bytes use Node streams and never pass through JSON IPC.
const service = await gitManager.openRepositoryService({
org: 'foss.global',
repo: 'gitmanager',
service: 'upload-pack',
protocol: 'version=2',
});
sshChannel.pipe(service.stdin);
service.stdout.pipe(sshChannel);
service.stderr.pipe(sshChannel.stderr);
const result = await service.completed;
The returned service counts request, response, and stderr bytes, runs with an isolated Git environment, has its own process group and deadline, and is terminated when GitManager.stop() runs. Receive-pack uses the same private managed pre-receive hook and exact policy snapshot as Smart HTTP. The caller must authenticate the SSH key, authorize the repository operation, cap sessions, serialize writes with HTTP pushes and policy changes, inspect refs, persist the repository, record downstream events, and withhold the final receive-pack status until all durable work succeeds.
Bounded readiness
checkReadiness() performs one bounded, read-only probe. Lifecycle states return failure results instead of throwing; only an invalid timeoutMs throws a TypeError. The default timeout is 2000 ms, the maximum is 30000 ms, and an AbortSignal cancels the local wait.
const readiness = await gitManager.checkReadiness({ timeoutMs: 2000 });
if (!readiness.ready) {
console.error(`GitManager unavailable: ${readiness.reason}`);
}
When Git Smart HTTP is running on a loopback bind, the probe targets the engine's own GET /healthz route out-of-band, so readiness stays available while the serial management channel is busy with a long-running command such as a mirror import or archive creation. Without a loopback Smart HTTP route, including a service bound to a non-loopback address with allowUnsafeNonLoopback, one shared bounded getStatus round trip per instance is used; that fallback queues behind in-flight management commands and a Smart HTTP probe failure never falls through to it. Failure reasons are not_running, starting, stopping, aborted, timeout, and unavailable; the result matches the readiness contracts of @push.rocks/smartdata and @push.rocks/smartbucket.
Public API
GitManager: TypeScript facade for the Rust engine lifecycle.GitManager.start(): spawns the Rust binary throughsmartrustand starts the engine.GitManager.stop(): asks the engine to stop and always kills the bridge process in cleanup.GitManager.getStatus(): returns{ running, dataDir, uptimeMs, smartHttp? }.GitManager.checkReadiness(): performs a bounded, cancellable read-only readiness probe and returns{ ready, status, durationMs }plus a failurereasonwhen unavailable.GitManager.setSmartHttpRepositoryAccess(): atomically replaces the running Smart HTTP allow-all/allowlist state.GitManager.setSmartHttpRepositoryVisibility(): applies one revision-ordered repository visibility update.GitManager.setSmartHttpRepositoryProtectedRefs(): applies a complete revision-fenced set of exact protected-head rules and returns its effective revision and digest.GitManager.initBareRepository(): creates an empty local bare repository.GitManager.importMirror(): imports a remote repository withgit clone --mirror. Supported remote forms arehttps://,http://,ssh://,git://,file://, and SCP-likegit@host:org/repo.gitURLs.GitManager.refreshMirror(): atomically fetches and verifies the configured mirror source.GitManager.verifyRepository(): runs a strict full integrity check.GitManager.createRepositoryBundle(): writes a bounded full Git bundle intotransferDirand returns its temporary path and size.GitManager.createRepositoryArchive(): writes a bounded deterministic exact-committar.gzarchive intotransferDirand returns its digest, compressed size, extracted regular-file size, entry count, and temporary path.GitManager.restoreRepositoryBundle(): validates and atomically restores a bounded bundle fromtransferDir.GitManager.openRepositoryService(): opens a bounded upload-pack or protected receive-pack stream for an authenticated SSH-style ingress.GitManager.listRepositories(): lists local repositories managed below the configured data directory.GitManager.listRefs(): lists branch and tag refs.GitManager.listTree(): lists tree entries at a ref and optional path.GitManager.readFile(): reads a blob as base64 after checking the blob size limit.GitManager.listCommits(): lists commit summaries for a ref.GitManager.getCommit(): reads commit detail metadata, parents, author/committer data, and diff stats for a ref.GitManager.getDiff(): returns a structured unified diff between a head ref and its first parent or an explicit base ref.GitManager.listTags(): lists lightweight and annotated tags with target object metadata.GitManager.getTag(): reads one tag by exact name.GitManager.running: local facade running state.RustGitManagerBridge: lower-level typed bridge around@push.rocks/smartrust.IGitManagerOptions,IGitManagerStatus,IGitManagerReadinessOptions,TGitManagerReadinessResult,TGitManagerReadinessFailureReason,IRustGitManagerConfig,IGitManagerProtectedRefRule,IGitManagerSmartHttpRepositoryProtectedRefs,TGitManagerProtectedRefsApplyStatus, andIGitManagerProtectedRefsApplyResult: exported option, status, readiness, and protected-ref contracts.
Repository/read DTO contracts are re-exported from the published @foss.global/interfaces package.
Read/list APIs and raw-service preparation go through JSON IPC. Git packfile streaming uses controlled git upload-pack and git receive-pack subprocesses behind bounded HTTP or Node streams; packfiles do not pass through JSON IPC.
Read APIs and Smart HTTP use the system git CLI with explicit arguments, non-interactive environment variables, command timeouts, ref-to-object-id resolution, path validation, bounded stdout/stderr capture, ref/tree result caps, and a 1 MiB file read cap. Receive-pack streams bounded requests, replaces repository-configured hooks with GitManager's verified managed pre-receive hook, enables incoming object fsck, and rejects the complete push when policy evaluation cannot complete safely.
Rust Build
The package builds a Rust binary named rustgitmanager from rust/ using @git.zone/tsrust.
Configured targets:
linux_amd64linux_arm64
The ARM64 target uses rust/.cargo/config.toml with aarch64-linux-gnu-gcc as linker.
Testing
pnpm test
pnpm run test:rust
pnpm run build
pnpm test runs the pretest hook first, which builds the Rust binaries with tsrust, and then runs lifecycle, bounded readiness, local mirror/read API, raw upload-pack/receive-pack preparation, clone/fetch denial, trusted receive-pack denial, explicit policy initialization, protected create/delete/non-fast-forward enforcement, inherited self-test hardening, restart replay, and real push tests through smartrust.
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
Bundled Rust binaries include third-party Rust crates under their respective licenses. See thirdparty.md for the packaged notice list.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.