fs
You need to pass a file system into isomorphic-git functions that do anything that involves files (which is most things in git).
In Node, you can pass the builtin fs module.
In the browser it's more involved because there's no standard 'fs' module.
But you can use any module that implements enough of the fs API.
Node's fs
If you're only using isomorphic-git in Node, you can just use the native fs module:
const git = require('isomorphic-git');
const fs = require('fs');
const files = await git.listFiles({ fs, dir: __dirname });
console.log(files)
LightningFS
If you are writing code for the browser, you will need something that emulates the fs API.
While ZenFS (see next section) has more features, LightningFS might very well fit your needs.
It was designed from scratch for isomorphic-git (by the same author) to eek out more performance
for fewer bytes. As an added bonus it's dead simple to configure.
<script src="https://unpkg.com/@isomorphic-git/lightning-fs"></script>
<script src="https://unpkg.com/isomorphic-git"></script>
<script>
const fs = new LightningFS('my-app')
const files = git.listFiles({ fs, dir: '/' });
console.log(files);
</script>
You can configure LightningFS to load files from an HTTP server as well, which makes it easy to prepopulate a browser file system with a directory on your server. See the LightningFS documentation for an example of how to do this.
ZenFS
At the time of writing, the most complete option is ZenFS. It has a few more steps involved to set up than in Node, as seen below:
<script type="importmap">
{
"imports": {
"isomorphic-git": "https://esm.sh/isomorphic-git",
"@zenfs/core": "https://esm.sh/@zenfs/core",
"@zenfs/dom": "https://esm.sh/@zenfs/dom"
}
}
</script>
<script type="module">
import { fs, configureSingle } from "@zenfs/core";
import { IndexedDB } from "@zenfs/dom";
import git from "isomorphic-git";
await configureSingle({ backend: IndexedDB });
const files = git.listFiles({ fs, dir: '/' });
console.log(files);
</script>
Besides IndexedDB, ZenFS supports many different backends with different performance characteristics (all backends support sync operations), as well as different features such as proxying a static file server as a read-only file system, mounting ZIP files as file systems, or overlaying a writeable in-memory filesystem on top of a read-only filesystem. You don't need to know all these features, but familiarizing yourself with the different options may be necessary if you hit a storage limit or performance bottleneck in the IndexedDB backend I suggested above.
An advanced example usage is in the old unit tests for isomorphic-git.
It uses the Fetch backend to mount (read-only) the test fixtures directory which is stored on the server, then adds a read-write InMemory layer using the Overlay backend so that the tests can modify files locally.
In between tests it empties the InMemory, restoring the file system to a pristine state.
The current unit tests use LightningFS instead, which was built with this HTTP-backed overlay behavior by default, because I find it so useful.
Environments without IndexedDB (e.g. Cloudflare Workers, Deno Deploy)
Cloudflare Workers, Deno Deploy, and other edge runtimes do not expose IndexedDB.
This means that LightningFS throws a ReferenceError: indexedDB is not defined at
startup, and ZenFS's IndexedDB backend won't work either.
The good news is that isomorphic-git itself has no dependency on IndexedDB — it
just needs any object implementing the fs.promises interface.
The fix lives entirely in the filesystem layer.
Option 1 — LightningFS with a MemoryBackend (recommended)
LightningFS is designed to be storage-agnostic. Its DefaultBackend uses IndexedDB
under the hood, but you can swap it out via the backend option with any object that
implements five low-level methods: saveSuperblock, loadSuperblock, readFile(inode),
writeFile(inode, data), and unlink(inode).
A MemoryBackend backed by a plain Map requires no platform APIs and works everywhere:
// MemoryBackend.js
class MemoryBackend {
constructor() {
this._map = new Map()
}
saveSuperblock(superblock) {
this._map.set('!root', superblock)
}
loadSuperblock() {
return this._map.get('!root') || null
}
readFile(inode) {
return this._map.get(inode) || null
}
writeFile(inode, data) {
this._map.set(inode, data)
}
unlink(inode) {
this._map.delete(inode)
}
async wipe() {
this._map.clear()
}
}
Pass it to LightningFS via the backend option:
import LightningFS from '@isomorphic-git/lightning-fs'
import git from 'isomorphic-git'
import http from 'isomorphic-git/http/web'
import { MemoryBackend } from '@isomorphic-git/lightning-fs' // once the PR is merged
// or paste the class above locally in the meantime
const fs = new LightningFS('mem', { backend: new MemoryBackend() })
await git.clone({
fs,
http,
dir: '/',
url: 'https://github.com/example/repo',
singleBranch: true,
depth: 1,
})
Note: Data is ephemeral — it lives in the JS heap and is lost when the runtime terminates. For persistence across Cloudflare Worker requests, see Option 3 in the Cloudflare Workers guide to replace
MemoryBackendwith a backend backed by Durable Object storage using the same five-method interface.
Option 2 — ZenFS with the InMemory backend
ZenFS provides an InMemory backend that also avoids
IndexedDB and implements the full fs.promises API:
import { fs, configureSingle } from '@zenfs/core'
import { InMemory } from '@zenfs/core'
import git from 'isomorphic-git'
import http from 'isomorphic-git/http/web'
await configureSingle({ backend: InMemory })
await git.clone({
fs,
http,
dir: '/',
url: 'https://github.com/example/repo',
singleBranch: true,
depth: 1,
})
Compatibility notes
| Runtime | LightningFS (default) | LightningFS + MemoryBackend | ZenFS InMemory |
|---|---|---|---|
| Node.js | ❌ (no IndexedDB; use native fs) | ✅ | ✅ |
| Browser | ✅ | ✅ | ✅ |
| Cloudflare Workers | ❌ (no IndexedDB) | ✅ | ✅ |
| Deno Deploy | ❌ (no IndexedDB) | ✅ | ✅ |
| Bun | ✅ | ✅ | ✅ |
Implementing your own fs
There are actually TWO possible interfaces for an fs object: the classic "callback" API and the newer "promise" API. If your fs object provides an enumerable promises property, isomorphic-git will use the "promise" API exclusively.
Using the "callback" API
A "callback" fs object must implement the following subset of node's fs module:
- fs.readFile(path[, options], callback)
- fs.writeFile(file, data[, options], callback)
- fs.unlink(path, callback)
- fs.readdir(path[, options], callback)
- fs.mkdir(path[, mode], callback)
- fs.rmdir(path, callback)
- fs.stat(path[, options], callback)
- fs.lstat(path[, options], callback)
- fs.readlink(path[, options], callback) (optional ¹)
- fs.symlink(target, path[, type], callback) (optional ¹)
- fs.chmod(path, mode, callback) (optional ²)
- fs.rm(path[, options], callback) (optional ³)
Internally, isomorphic-git wraps the provided "callback" API functions using pify.
As of node v12 the fs.promises API has been stabilized. (lightning-fs also provides a fs.promises API!) Nowadays, wrapping the callback functions
with pify is redundant and potentially less performant than using the native promisified versions. Plus, if you're writing your own fs implementation,
the fs.promises API lets you write straightforward implementations using async / await without the messy optional argument handling the callback API needs.
Therefore a second API is now supported...
Using the "promise" API (preferred)
A "promise" fs object must implement the same set functions as a "callback" implementation, but it implements the promisified versions, and they should all be on a property called promises:
- fs.promises.readFile(path[, options])
- fs.promises.writeFile(file, data[, options])
- fs.promises.unlink(path)
- fs.promises.readdir(path[, options])
- fs.promises.mkdir(path[, mode])
- fs.promises.rmdir(path)
- fs.promises.stat(path[, options])
- fs.promises.lstat(path[, options])
- fs.promises.readlink(path[, options]) (optional ¹)
- fs.promises.symlink(target, path[, type]) (optional ¹)
- fs.promises.chmod(path, mode) (optional ²)
- fs.promises.rm(path[, options]) (optional ³)
¹ readlink and symlink are only needed to work with git repos that contain symlinks.
² Right now, isomorphic-git rewrites the file if it needs to change its mode. In the future, if chmod is available it will use that.
³ Only called with recursive: true option. A fallback implementation is provided if not implemented.