PushStack Docs
git-fs-s3

The ObjectStore interface

The five-method contract every storage backend implements, and how to write your own.

Everything in git-fs-s3 is built on one small interface. createGitFs doesn't know or care whether the bytes underneath come from R2, S3, memory, or something else entirely — it only calls these five methods.

interface ObjectStore {
  get(key: string): Promise<Uint8Array | null>;
  getRange?(key: string, start: number, end: number): Promise<Uint8Array | null>;
  put(key: string, data: Uint8Array): Promise<void>;
  delete(key: string): Promise<void>;
  head(key: string): Promise<ObjectStat | null>;
  list(prefix: string, options?: ListOptions): Promise<ListResult>;
}

The five methods above are required. getRange is optional — implement it if your backend can read a byte range without fetching the whole object.

A missing key is null, never a thrown error. Every layer in the stack — the git-aware fs, the cache, the retry store — relies on that to distinguish "doesn't exist" from "the backend failed."

Method contracts

functionget
get(key: string): Promise<Uint8Array | null>
ParameterTypeDescription
keystringThe full object key.

Returns: Uint8Array | null

The object's bytes, or null if the key doesn't exist. Never throws for a missing key.

functiongetRange
getRange?(key: string, start: number, end: number): Promise<Uint8Array | null>
ParameterTypeDescription
keystringThe full object key.
startnumberFirst byte to return, inclusive.
endnumberLast byte to return, inclusive.

Returns: Uint8Array | null

Optional. Bytes start..end of the object, inclusive at both ends, or null if the key doesn't exist. A store that can't serve ranges leaves the method undefined, and getObjectSize then reports null ("size unknown") rather than fetching a whole pack to answer. That fallback is the point: range reads are what let a caller ask is this object too large? without paying for the pack that holds it. Exposed on the git-aware fs as fs.promises.getRange, again only when the backing store has it, and forwarded by createCachedStore straight to the backing store — a few bytes at a known offset gain nothing from the cache.

functionput
put(key: string, data: Uint8Array): Promise<void>
ParameterTypeDescription
keystringThe full object key.
dataUint8ArrayThe bytes to store.

Writes (or overwrites) an object. Object storage has no atomic multi-key transactions — a caller that needs several keys to become visible together needs its own coordination (see Semantics & limitations).

functiondelete
delete(key: string): Promise<void>
ParameterTypeDescription
keystringThe full object key.

Removes an object. Deleting a key that doesn't exist is not an error.

functionlist
list(prefix: string, options?: ListOptions): Promise<ListResult>
ParameterTypeDescription
prefixstringOnly keys starting with this prefix.
options?ListOptionsdelimiter (directory-style grouping) and limit.

Returns: ListResult

{ objects: { key, size }[], prefixes: string[] }. When options.delimiter is set (git-fs-s3 always uses "/"), keys are grouped: anything after the first delimiter past prefix collapses into one entry in prefixes instead of appearing in objects — this is what makes readdir() work against storage that has no real directories.

Writing your own store

Implement the interface above for any backend and pass it straight to createGitFs — every other piece (caching, retry, the git-aware layer, /http, /ops) works unmodified on top of it.

MemoryObjectStore (exported from the root package) is the reference implementation — read it first. It's also the store used throughout this package's own test suite, and the one you'll reach for in yours:

import { createGitFs, MemoryObjectStore } from 'git-fs-s3';

const fs = createGitFs(new MemoryObjectStore());

Get list()'s delimiter semantics right or nothing above it works — isomorphic-git calls readdir() constantly, and readdir() is built entirely on list(prefix, { delimiter: '/' }). Compare your implementation's behavior against MemoryObjectStore's for the same inputs before trusting it.

S3ObjectStore

The one storage backend shipped out of the box, over @aws-sdk/client-s3 (an optional peer dependency, loaded only via the /s3 subpath so consumers who don't need it don't pay for it).

import { S3ObjectStore } from 'git-fs-s3/s3';

const store = new S3ObjectStore({
  client, // reuse one S3Client — see the note below
  bucket: 'my-git-repos',
  prefix: 'repos/', // optional: a fixed prefix under every key
  contentType: (key) =>
    /\/(HEAD|config)$|\/refs\//.test(key) ? 'text/plain' : undefined,
});

Reuse one S3Client across every store you create — a busy repo page can mean hundreds of object reads, and a shared client pools HTTP connections instead of reconnecting per store.

S3ObjectStore implements the optional getRange (an S3 Range header, inclusive at both ends), which getObjectSize uses to answer a size without fetching the object.

On this page