Designing a git alternative - nit
This is a more technical post, and I’m gonna dive into details without much explanation on the problems nit solves. If you want to understand why nit was designed this way, I recommend reading my other post - Building nawc and nit - first. Alternatively, you can watch this excellent talk by Theo:
It’s Time To Rethink Everything by @t3dotgg
The snapshot DAG
With nit, a lot of inspiration is taken from another version control system called Jujutsu (JJ), and so if you’re familiar with it, you’ll see a lot of similarities between the two.
In nit, there’s no concept of staging or commits. Instead we have “snapshots”. A snapshot, as its name suggests, represents a snapshot in time of your files.
An important concept in nit, is that snapshots carry permissions - read (r), write (w), and execute (x). Read is self explanatory. It control which users can see the snapshot. Write controls who can “branch off” the snapshot. In other words - if a user lacks write permission for snapshot foo, they can’t create a snapshot that has foo as a parent. The git way to think about this would be something like - not everyone can push to main. Execute controls who can manage permissions to the snapshot. A user without an execute permission can’t add a new reader to the snapshot.
By design, the snapshot DAG is a CRDT. Meaning operations on it are append-only. There’s no way to delete or update a node from the DAG, only add one.
To avoid conflicts, only one writer is allowed for a snapshot. Once there’s more than one writer, the DAG becomes “append-only”, meaning you can’t edit the snapshot - only branch off it.
Internally, when there’s only one writer, the snapshots are actually built from multiple sub-snapshots. Sub-snapshots are only shared with the owner of the snapshot, the other readers receive a “squashed” view. This is also incredibly useful when working with AI agents.
Well what if I want to delete a snapshot? Simple - remove all of its readers. If a snapshot doesn’t have readers it’ll eventually get cleaned up.
nit is everywhere, all at once
Unlike git, nit doesn’t exactly live in your codebase, and doesn’t need you to run a command to pull the latest worktree from a remote.
Instead, it has a long running process managing all of that machine’s nit repos, and views which I’ll explain in a bit.
To me that means one important thing: As long as the nit process is running, your code is up to date with everyone else. And given the CRDT nature, a remote like Github is not even necessary. Eventually, all of the repo checkouts converge to the latest version.
An “always live” remote becomes useful for different reasons:
- To ensure the other users receive updates even if they’re never online at the same time.
- To have a public-facing view where anyone can view the source code.
If you ask me #2 is extra cool, because a remote then becomes a user just like any other. Meaning, you can also control its permissions:
- You don’t have to open source all of your code anymore. Only the parts you give the remote read access to.
- You can have multiple remotes, and they’re always in sync. Extra backups never hurt anyone.
- You can have multiple remotes with different permissions.
Remotes are still a special concept in nit, as they’re allowed to receive updates even about snapshots they don’t have read permissions for. Just to enable the transit.
This is enabled by an important feature that hasn’t been mentioned so far - read permissions are enforced by the same end-to-end encryption that powers apps like Signal. Meaning even if a user manages to get access to your remote they won’t be able to read the repo without the decryption key.
Views
The nit process isn’t tied to your file system. The actual implementation can be completely swapped out. It can be whatever - the operating system’s file system, an s3 bucket, a database. Hell, it can print them and require you to scan them if you need the extra physical exercise.
But when the time comes to interact with the repository, the user needs a way to do it conveniently. The way its done in nit is by creating a view of the repository using a view manager.
By default, nit will ship with OS view manager. It’ll just be a file directory being watched by the manager. This allows it to be used with your code editor.
But in theory, you can create any custom view manager you might need. For example, you can expose a cli based view manager, which can be used by AI agents without having to create a whole separate file system clone of the repo (nice alternative to worktrees).
Parallel Views
In JJ, two snapshots can be “merged” by creating a snapshot as a child of both of them. If the “merge” results in a conflict, it succeeds anyway, and the result is shown as content in the snapshot itself. In other words - conflicts are part of the process. nit borrows that idea from JJ (yoink).
But in addition, nit introduces an extremely powerful concept - parallel views.
Snapshots can have their own nitignore file, effectively allowing you to snapshot different parts of your codebase separately. When creating a view from two parents that don’t have any files in common, the default view manager creates a separate snapshot for every parent, and allows you to edit both of them simultaneously.
It is best illustrated by an example: Let’s say you want to share your .env across your devices. Below is how you’d do it in nit.
- The root of a nit repo is always empty. Branch out of it, creating a snapshot containing just a
.envfile, and nothing else. - By default, you’ll be the only user with read permissions for that snapshot. Meaning - only you can see that .env file, and it’s enforced by end-to-end encryption.
- Let’s assume you want to work on another snapshot that ignores the
.envfile, or simply doesn’t have it. Create a view of both snapshots. - Since they don’t share any files, nit will not “merge” them into one snapshot. Instead, you’re able to edit both of them in parallel. Changes to the
.envfile result in creating a child of the env snapshot. Changes in any other file results in creating a child of the other snapshot.
To simplify this process, nit also borrows the idea of bookmarks from JJ. In nit, you can bookmark a view. Adding a child to one of the snapshots bookmarked moves the bookmark to that child automatically. This allows you to switch between views and parallel views with ease.
Important note - moving bookmarks requires an execute permission.
Permission Labels and Publishing
It would probably get annoying really quickly if you had to set permissions individually for every single snapshot. Luckily, that’s not the case.
Permission labels give names to permission configurations. For example, you can quickly make a private label that gives read and write permission to everyone on your team. Then that label can be applied quickly to a snapshot using nit publish private.
nit publish <labels> sets the permissions of the current snapshot to an and operation between the permissions of the parent snapshots and the labels specified.
As an example, let’s say you have an open source project, but you don’t want to open source all of it. Let’s say you have a package called “secret” you want to keep private.
- Create a snapshot with root as a parent, and initialize the secret package there
- Create a
privatelabel, and apply it to that snapshot usingnit publish private - Assuming you have a
mainbookmark that creates a parallel view of your entire project, add the new secret package snapshot to that bookmark - Next time you make changes to the secret package, a snapshot will be created as a child of its snapshot. By default, that child will only give you read permissions, and will only be synced across devices where you’re signed in.
- Once you’re done with the changes,
nit publishwill inherit theprivatepermissions from the parent snapshot, be synced to other users with read permission, and move the bookmark up to it.
Discovered Behavior
These are some features this design allows, but it wasn’t intentional. I just discovered them as I was designing nit:
- Pull requests don’t have to be a separate feature - You can specify snapshots to give public write permission, and just not give public execute permission. This allows for anyone to contribute to the project. Then “merging a pull request” is equivalent to moving a bookmark to that snapshot. This also allows you to only allow contribution to certain parts of the projects, or only allow a select group of people to contribute.
- Monorepos - Have a parallel view with a snapshot sub-graph per package.
- Mega Monorepos - An unfinished feature design is the ability to “attach” the root of any nit repo to a snapshot in another one. Expanding on the previous point - this allows you to have all of your projects from different organizations under one repo, and parallel views and bookmarks allow you to share code between them and jump around them with ease.
Please Be Patient With Me
Coding is not even my full time job. As of right now, this is a side project. It’s currently in its very early stages. In fact, as of writing this post, it’s an almost empty rust project. Even though I’m extremely excited about it, I still have my limits. I’m just one person, without a team.
Although I don’t expect it, I think this idea is cool enough that I wouldn’t be surprised if it gains traction. If so, I’ll try to update everyone on this blog and on my twitter profile. If enough people actually want to contribute and it’s not at a good enough state to host on nit itself, ironic as it is, we’ll create a git repo.
Finally - if you’re reading this, you’re probably early. If you care enough and want to influence the future of this project, please do hit me up. I’m always open to learn from other people and take suggestions.