DEV Community

Cover image for I built a browser-only Git diff viewer using File System Access API — no server needed
Kazuki Chigita
Kazuki Chigita

Posted on

I built a browser-only Git diff viewer using File System Access API — no server needed

When working with AI coding agents like Claude Code or Cursor, I often find myself reviewing diffs across multiple repositories at the same time. The agent touches several projects in one session, and I need to quickly see "what changed where" without switching between terminals or setting up complex tooling.

Existing tools are either single-repo, require a server, or need a local install. So I built Duff.

🔗 Demo: https://chigichan24.github.io/duff/
📦 Source: https://github.com/chigichan24/duff

What is Duff?

Duff is a Git diff viewer that runs entirely in your browser. No server, no CLI, no extensions — just open the URL and pick your local Git repositories.

  • 🔍 View diffs and modified files across multiple repos at once
  • 📊 Interactive commit history graph with range selection
  • 🖼️ Visual image diff with pixel-level comparison
  • 🔄 Auto-refresh to detect changes as you work
  • 💾 Workspace persists across sessions via IndexedDB

How it works

The key insight is combining two browser APIs that aren't often used together:

  1. File System Access API (showDirectoryPicker()) gives the browser direct read access to your local filesystem
  2. isomorphic-git provides a pure JavaScript Git implementation that works in the browser

I wrote a custom adapter that bridges the two — translating isomorphic-git's Node-style fs calls into File System Access API operations.

Browser UI → gitService → isomorphic-git → fsaAdapter → File System Access API → your local .git
Enter fullscreen mode Exit fullscreen mode

Technical hurdles I didn't expect

1. instanceof doesn't work the way you'd think

The File System Access API returns FileSystemFileHandle and FileSystemDirectoryHandle objects. My first instinct was:

if (handle instanceof FileSystemFileHandle) { ... }
Enter fullscreen mode Exit fullscreen mode

This works fine in production, but completely breaks in test environments (Playwright mocks). The fix was using the kind property instead:

if (handle.kind === 'file') { ... }
Enter fullscreen mode Exit fullscreen mode

2. Read-only operations aren't actually read-only

I assumed showing a git status would only need read access. Wrong. isomorphic-git's statusMatrix writes to .git/index as part of its comparison process. This meant my FS adapter needed full write support (createWritable) just to display a diff.

3. File System Access API is ~100x slower than native fs

Every file read goes through the browser's security layer. For a Git repo with hundreds of files, this adds up fast. I had to implement multiple layers of caching:

  • Ref resolution cache (5s TTL) — avoid re-resolving HEAD → commit hash on every operation
  • isomorphic-git object cache — shared across all operations to prevent re-parsing packfiles
  • Adapter instance cache (WeakMap per handle) — avoid recreating the fs adapter
  • No-op update guards — skip React re-renders when nothing actually changed

4. Persistence is tricky

FileSystemDirectoryHandle objects can be stored in IndexedDB (they're structured-cloneable), so your repo selection survives page reloads. But the browser will revoke the permission — you need to call queryPermission() / requestPermission() again on next visit.

Limitations

  • Chromium-only: Firefox and Safari don't implement the File System Access API (and have stated they won't, citing security concerns)
  • Performance on large repos: The FSA API overhead makes it impractical for very large repositories
  • No push/pull: This is a viewer, not a full Git client

Stack

  • React 19 + Vite
  • isomorphic-git
  • diff2html for diff rendering
  • pixelmatch for image diffs
  • idb-keyval for IndexedDB persistence
  • Hosted on GitHub Pages (zero-cost)

Try it

👉 https://chigichan24.github.io/duff/

Open it in Chrome/Edge/Arc, click "Add your first repository", and pick any local Git repo with uncommitted changes.

If you find it useful, a ⭐ on GitHub would mean a lot!

I'd love to hear your feedback — what would make this more useful for your workflow?

Top comments (0)