GitRoot

Craft your forge, Build your project, Grow your community freely

The teenager starts building its own future

If you only have 5 minutes

A lot has been done in this new release of GitRoot. When I saw other FOSS projects claiming “this is the biggest release”, I thought it was a joke. But in fact, it’s true: every new release gets bigger and bigger. It’s just astonishing to start a sprint with almost zero ideas and, 4 months later, see all of this accomplished.

If you skim this article, don’t forget to check out the conclusion about “what’s next?”.

The thing i’m most proud of

Executors can finally build GitRoot! As I love to say: GitRoot can build GitRoot on GitRoot. And it was a pain to do. Read more.

The thing with the most visibility

The plugin registry is here! After a lot of reading and reflecting, I started this “epic story”. Today is only the beginning, but you can now find plugins directly at gitroot.dev/plugins. What’s next? Read more.

The thing that changes everything but was so simple

Previously, a branch (and graft) couldn’t be rendered on the web. The problem was: if I have an index.md in my git that renders at myinstance.com/index.html, anyone can create a branch. If a “bad user” comes along, changes everything in this file, creates a branch, and pushes for review, where should this file render? Certainly not at myinstance.com/index.html. Read more.

The thing that was more complex than I thought

Apex was a big plugin. Basically, it manages the rendering of your git files to the web. But over time, it started to grow out of control because of markdown, code, worktrees, emojis… A simple (in theory) solution was to split it into multiple plugins. Read more.

The things I’m just happy are done

No more mandatory signed commits! Well, it’s still better if you sign them, but this change means the .gitroot/init.sh has been deleted moved to a plugin. Read more.

Moving a file from git to web was hard, deleting a file was “not implemented”, and a lot of APIs were missing in the plugin-sdk. I got my hands dirty with all that, and now everything is running smoothly. Read more.

In-depth Details

Executor

When I started this feature, I just wanted a lightweight container to run make build and output the result as a gitroot-next canary binary. If I had known how much time and energy it would actually cost, I’m not sure I would have taken this road.

At first, I chose bubblewrap as a lightweight container, and immediately started building a container version. Then I realized I needed a bareMetal one. And then, I needed a way to run it on other machines, so ssh came along.

But when I launched it for the first time, I discovered that having make isn’t enough for GitRoot, I need Golang, Rust, TinyGo, and so on. “No problem,” I thought, “I’ll use mise to manage that.” But running mise install takes a lot of time (like 10-15 minutes on my computer). So, I built a cache system to ensure that hit only happens the first time.

Still not the end! After that, I needed to configure port numbers, so I built a complete environment variable system, and used it to configure some mise paths.

Is it finished? Not yet! To be able to move a binary generated from a bwrap environment to the web, it has to travel a lot. And by “a lot,” I mean moving it to a place where the plugin that launched the executor can access it: its own cache. Then the plugin needs to move it from there to the web. No problem, I just added a bunch of SDK API.

After all that, some commands finally started working… well, kind of. When I ran a bwrap or a container command, I couldn’t get the stats of what was happening inside. Having process stats is something I’m proud of (even if it’s still evolving in future versions), but seeing that bwrap reported 20 MB of RAM while the sub-process was actually consuming 80 MB was very frustrating. Furthermore, I needed a way to extract stats from the remote SSH executor.

I tried a lot of things, but ultimately, I chose to build an executor CLI to handle it. I now inject that binary inside your bwrap, container, or SSH environment. This little CLI launches commands, gathers system stats, command status, and logs. It packs everything into a well-formatted JSON and returns it to the executor in GitRoot. GitRoot then manages artifacts/caches and hands the result back to the calling plugin (probably hop), which tells you what’s happening. In this case, it reports back so the grafter plugin can include the info in the current graft.

Exec cmdsbwrap/container/sshstatus/code/logs/statsstatus/code/logs/statsstatus/code/logs/statsHopGitRootexecutor clicmdexec

Phew, is that enough now? It should be, but we’ll see how it performs in real-time on gitroot.dev. Not everything is finished: for example, the SSH executor doesn’t manage cache yet #94af and can’t use the container strategy #f050. You can’t run the executor on other architectures #4d49 yet, and I’m sure you’ll soon want a way to clean up temp directories #647f.

But hey, let’s celebrate what we’ve built before jumping into the next sprint!

Plugin registry

Until now, explaining GitRoot was always about “data in git and the plugin system”, but what exactly are the plugins? Where are they? What can they do? All of that was a bit mysterious. With this milestone, I’m answering some of those questions with a first shot at a registry. Be careful, though: this first version is just a “hand-written” preview of what it could be in the future. If you look at the source, you’ll see that everything is ready to be automated, but nothing is… yet.

I’m not entirely sure what you, as a consumer, want to see here. Probably an explanation of what the plugin does, some docs, and the available versions. But as a plugin creator, you likely want to manage all of that in your own repository instead of the gitroot.dev registry. I chose to keep the registry very light and put the details directly in the plugin’s directory to give you an idea of what your own repo could look like.

I’ll need to iterate on this. Even if I choose to embed package details in the registry, I still need to decide how: maybe via a JSON in your repo, maybe in the registry itself, or perhaps inside your package (WASM)? I don’t know yet what’s simplest for you and for the registry.

Anyway, I hope this registry helps you understand what a plugin is and how it shapes your forge. Some are small (ladybug), others are massive (apex), and some aren’t usable solo as they are dependencies for others (like apx_mermaid).

Since plugins are now decoupled from GitRoot, I’ve moved all changelogs from the general one to the individual plugin directories. This way, I’ll be able to release plugin versions independently of GitRoot (and vice versa). I hope to break the plugin API less often, even if I know I probably will in the near future. All of this paves the way for you to build your own plugins and I hope to see them in the gitroot.dev registry soon. Or, of course, you can just build your own registry!

Apex render branch

For as long as I can remember, this has been a problem with GitRoot: how to render arbitrary changes, especially deletions, on the web? I had imagined a lot of strategies. The last one was to render inside a directory named after the git tree ID (the abstract representation of a working directory). Then, I’d create a symlink between that tree and a main directory.

The idea was: if you push a file in a branch, git gives you a tree ID like a654fs. It would be represented as mydomain.com/myBranch/file -> mydomain.com/tree/a654fs/file. When you merged that branch, GitRoot would simply symlink the main directory to it: mydomain.com/file -> mydomain.com/tree/a654fs/file.

Elegant, but honestly, a total over-engineered mess. When I actually sat down to code this, I ended up adding just two lines to the apex plugin:

1if p.currentCommit.Branch != "" && p.currentCommit.Branch != p.config.defaultBranch {
2	path = filepath.Join(p.config.branchesDir, p.currentCommit.Branch, path)
3}

And… yeah, that was enough. I’ve been overthinking this since the very beginning of GitRoot! Rendering files from branches into a dedicated branches directory… It’s simple. Just like GitRoot should be.

So, what does it mean in practice? The file is now rendered at myinstance.com/mybranch/index.html and the graft at myinstance.com/mybranch/grafts/mybranch.html. How did I not think of something this simple before?

Plugin deps

The catalyst for this development was the apex plugin. At over 3 MB, it was by far the largest plugin. It was packed with configuration, specifically a bunch of booleans to toggle features. Furthermore, I needed to render Markdown in other plugins, like pollen, to create a proper RSS feed. It became clear to me: this plugin needed to be split into smaller, more specialized ones.

So, I started building the apex_markdown plugin, then apex_code, and next week apex_mermaid. But what I hadn’t fully anticipated was the sheer amount of work required to make it all work together.

What if a dependency is missing? What if a method is deprecated or removed in a newer version? To handle this, I’ve chosen to make all dependencies entirely optional for now. It’s an easy choice to make today, but I might change my mind tomorrow, after all, there’s a reason most package managers handle version ranges and resolution.

This new architecture allows for adding tons of features without rebuilding an entire plugin. Imagine adding Org-mode or RST support: you just swap out apex_mardown instead of touching the whole stack. It also allows other plugins to reuse existing behaviors effortlessly.

With this in place, I’m not even sure if I’ll keep the report system, which was an other way of passing information between plugins.

Sigma plugin

Signed commits should be the default in git today. With SSH signing, it’s easy to do, and everyone should use it because impersonating a name and email is way too easy in git. When I started GitRoot, I made signed commits mandatory before allowing a push. But in fact, making signatures mandatory should be a choice for the instance owner. GitRoot is all about the freedom to choose how your forge works for you.

Furthermore, a friend told me he preferred signing with PGP and already had his setup ready. So why force him to use a .gitroot/init.sh? It’s frustrating when you want to test a new forge and you’re forced to run random sh commands, even if it’s just to configure your git client.

With this release, I have completely removed init.sh from GitRoot itself. But of course, as always, I’ve created a plugin to “almost” recreate this feature. The only difference is: instead of refusing your commit when you push, the stigma plugin will simply report unsigned or badly signed commits in your graft.

Now, you have the choice to accept unsigned commits in your repo. You can even ignore the whole signing topic entirely if you don’t install the stigma plugin.

That said, I’m still convinced that signing commits should remain the default.

FS plugin-sdk

After finishing all the executors features, one thing was still missing: the ability for a plugin to move a file from one path to another. As explained in this blog post, even though a plugin sees one single filesystem, GitRoot actually uses three different ones: one for cache (a directory), one for the web (a subdirectory), and a virtual one for the git working directory.

I was missing several methods in both the Rust and TS SDKs, and the existing API was poorly designed with inconsistent naming. I decided to redesign everything and added a lot of new methods accessible via FS in the SDK. Now, you can copy, move, delete, replace, and write files in every filesystem. Moving and copying can now be done between different filesystems, making it easy to make a file accessible on the web even if it originated from git or the cache.

I have marked all old methods as deprecated, and they will be removed in the next version of GitRoot (or more specifically, the plugin-sdk, though they share the same release number for now).

What next?

I have a massive roadmap for GitRoot (in no particular order):

Yeah, I could go on all night. And those are just core GitRoot features. I haven’t even mentioned making instances to build projects in hosted environments. I haven’t talked about specific plugins (sending a “toot” on commit, managing secrets with age, translating your app…). I haven’t even touched on federating GitRoot with other GitRoot instances, or with Forgejo, SourceHut, or Radicle.

All of that is possible, and I’m confident it will be accessible in the future. But “when”? That’s the real question. If you think you can help in any way (coding, of course, but also management, communication, or just to brainstorm), please let me know.

This is only the beginning. GitRoot will get better and better with every release.