# gitfs commits every write including metadata, resolves upstream merges in your favour, and pins seven packages from 2019

> A FUSE filesystem from a WordPress hosting company that mounts a git branch as a directory and turns everything you do inside it into commits. The design is small and legible, the merge policy is one sentence long, and the dependency file has not moved in seven years.

**presslabs/gitfs** — Version controlled file system

- Repository: https://github.com/presslabs/gitfs
- Website: https://www.presslabs.com/code/gitfs/
- Stars: 2,600 · Forks: 163
- Language: Python
- License: Apache-2.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/presslabs-gitfs

## Every write becomes a commit, including metadata

The mechanism is one sentence long: gitfs is a FUSE filesystem that fully integrates with git, you mount a remote repository's branch locally, and any subsequent changes made to the files are automatically committed to the remote.

The feature list makes clear how far that goes. Changes to create, delete and update operations are committed, and so are metadata changes, which is the part that surprises people, since a chmod or a touch on a mounted file is a commit. You can browse through the working index and the commit history, and the history of the branch you are working on is exposed by simulating snapshots of every commit.

Two properties make that workable at scale. Caching commits reduces the memory footprint and speeds up navigation, and commits are batched, which reduces the number of pushes. So the cost model is a lot of small local commits with a slow trickle of pushes, rather than one commit per save going over the network.

## The upstream merge accepts local changes, and that is the whole policy

One line in the feature list carries more risk than the rest of the file: merges with upstream by automatically accepting local changes.

There is no qualifier, no option name and no strategy in the usage examples. The options shown are `repo_path`, `branch`, `log`, `debug`, `foreground`, `fetch_timeout` and `merge_timeout`, none of which is a conflict policy, and the complete list lives on a separate arguments page rather than in this file.

For a mount with one writer that is exactly what you want, because a background fetch that introduced a conflict would otherwise need a human. For a directory that two people edit at once, it means the resolution is decided before anybody looks, and the losing content is not the one kept. That is the single behaviour to check before you point a shared mount at a branch.

## Three install routes, and a typo in each of two of them

Packages are provided for the major Ubuntu releases and for macOS, with community packages available for most popular Linux distributions, and the file invites you to contact the authors if you have built one so they can list it.

For Ubuntu 18.04 and later it is a personal package archive and the usual three commands:

```bash
sudo add-apt-repository ppa:presslabs/gitfs
sudo apt-get update
sudo apt-get install gitfs
```

macOS is `brew install gitfs`, and there is also a PyPI package installed with `pip install gitfs`. Two sentences in that section carry typos, the word commmand for command and easly for easily, and the usage examples end with an ellipsis after `merge_timeout=0.1...`, which is documentation shorthand rather than something to paste.

The project is by Presslabs, described as a managed WordPress hosting provider, which explains both the FUSE dependency and the audience.

## Seven dependencies, every one pinned with an equals sign

The requirements file is the shortest interesting file in the repository:

```
atomiclong==0.1.1
cffi==1.12.3
fusepy==3.0.1
pycparser==2.20
pygit2==0.28.2
raven==6.10.0
six==1.12.0
```

Seven packages, all pinned exactly rather than with a floor, which means a fresh install takes those precise versions and a rebuild after a security advisory takes the same versions until somebody edits the file. The set also dates the code: `six` is the Python 2 and 3 compatibility library, `cffi` and `pycparser` sit at their 2019 releases, and `raven`, the error reporting client, is a runtime requirement rather than a development one.

`pygit2` is the binding that does the actual git work and `fusepy` is the filesystem layer, so those two pins decide which git and which FUSE behaviour you get.

## Classifiers stop at Python 3.8, and the newest release is from 2019

The packaging metadata lists classifiers for Python 2.7 and then 3.4, 3.5, 3.6, 3.7 and 3.8, and declares platforms as any. The first of those is generous for a FUSE filesystem, since the filesystem layer is the point, and the second says nothing about which operating systems can host a FUSE mount.

The mechanics of the package are otherwise conventional: the version is imported from the package itself rather than written in the metadata, install requirements are read out of requirements.txt at build time, packages are discovered while excluding tests, the data is included, zip safety is off, and the console script is `gitfs = gitfs:mount`.

The version story is the interesting part. Releases 0.5.0, 0.5.1 and 0.5.2 were published on three consecutive days in October 2019, and the last push on the default branch is dated 2026-04-13. So the code has moved a long way past the newest tag while the distribution metadata still describes Python versions from five years ago.

## The build produces one self-contained file with pex

The Makefile is written for a Python project of a particular vintage and says so in its details. The build target runs `pex` with `--disable-cache`, the requirements file and the entry point, producing a single executable at `build/gitfs`, and `install` copies it to `$(DESTDIR)$(PREFIX)/bin/gitfs` with mode 0755, while `uninstall` removes that path.

Variable assignment is inconsistent in a way that matters. `PREFIX` is set to `/usr/local` with an immediate assignment, while `VIRTUAL_ENV`, `TESTS` and `PYTHON` use the overridable form and default to a virtualenv under the build directory, `tests` and Python 3.7. All four can still be overridden on the command line, but only the last three respond to an exported environment variable.

The lint target runs `black -t py27 gitfs`, asking for Python 2.7 compatible formatting, and `verify-lint` runs the formatter and then `git diff --exit-code`, so a formatting change fails the check rather than being committed.

## The docs target writes to your global git identity

One target deserves a warning. `gh-pages` depends on the docs target, and before copying anything it runs `git config --global user.email` and `git config --global user.name`, setting the name to a specific person and the address to a bot account on the project's domain. It then copies the generated `docs/index.html` into the root of the working tree, stages everything with `git add .` and commits with a message that begins with a marker for automated documentation.

On a workstation that rewrites the identity your own commits use, globally, and it stages the whole tree. That is a reasonable thing for a continuous integration agent and an unpleasant surprise on a laptop.

The rest of the tooling is visible in the tree and mostly explains the same division. There is a `.drone.yml` with a Drone badge, `.coveralls.yml` with a `.coveragerc`, a `Vagrantfile` and a `Dockerfile.test` for the test environment, `mkdocs.yml` with a `docs/` directory, and a `script/` directory holding the two test entry points. Tests work in `/tmp/gitfs-tests`, with mount and repository directories named from a random number.

## Conclusion

gitfs fits a workflow where a directory is edited by many people or by software that knows nothing about version control, and where losing the ability to organise commits by hand is the point rather than the problem. Before you mount anything shared, read the merge line again: upstream merges accept local changes automatically, so treat the mount as single writer unless you have checked what happens on a collision. And check the dependency pins before you deploy, because exact versions from 2019 mean you inherit their state rather than their fixes, on a branch whose last commit is far newer than its last release.

## FAQ

### What is gitfs?

A FUSE filesystem that fully integrates with git. You mount a remote repository's branch locally, and any subsequent changes made to the files are automatically committed to the remote.

### How do I mount a repository with gitfs?

Run gitfs with the repository to clone and a directory to mount it on, for example gitfs http://your.com/repository.git /mount/directory. Options are passed after -o and include repo_path, branch, log, debug, foreground, fetch_timeout and merge_timeout.

### How do I install gitfs on Ubuntu?

Add the personal package archive with sudo add-apt-repository ppa:presslabs/gitfs, then run sudo apt-get update and sudo apt-get install gitfs. Packages are provided for the major Ubuntu releases and for macOS.

### What happens when gitfs merges with upstream?

It merges with upstream by automatically accepting local changes. No conflict policy option appears in the usage examples, so the full list of options has to be checked on the arguments page.

### Does gitfs batch its pushes to the remote?

Yes. Caching commits reduces the memory footprint and speeds up navigation, and commits are batched to reduce the number of pushes.

## Sources

- [License: Apache-2.0](https://github.com/presslabs/gitfs/blob/master/LICENSE)
- [presslabs/gitfs on GitHub](https://github.com/presslabs/gitfs)
- [Project website](https://www.presslabs.com/code/gitfs/)
- [README](https://github.com/presslabs/gitfs/blob/master/README.md)
- [Releases](https://github.com/presslabs/gitfs/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/presslabs-gitfs
