GitRoot

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

Move slowly, and take care of things

These days, everything moves too fast. They push thousands of lines of AI generated code every hour, deploying it without even understanding how it works. But with GitRoot, I want to do the exact opposite. I want to take my time, savor every architectural choice, and thoroughly enjoy writing every line of code by hand. It’s a project that is deeply close to my heart: a space for freedom far away from the rat race of metrics. So yes, we move at our own pace: slowly, and take care of things.

Thanks to lonugem for the title idea

If you only have 5 minutes

Breaking PURL

I’m sorry to say that the first feature I want to talk about is a big breaking one. I know, I know, on the homepage of GitRoot I say “no headaches to upgrade my forge”…. but this time you will… If you are not happy, you can contact the support and get a refund for the previous year and a 100% discount for the next one :D

Joking aside, you need to replace your plugin URL with a PURL one. ECMA PURL is a specification to produce “a standardized URL-based syntax that uniquely identifies software packages”.

This corresponds to the following change:

1- url: https://gitroot.dev/releases/0.4.0/ladybug-0.0.4.wasm
2- checksum: sha256:7409837469ce89d5c9fd8dd48236764650f48523ff4d81295967154977a01635
3- name: ladybug
4- version: 0.0.4
5+ purl: pkg:gitroot/gitroot/ladybug@0.0.5?checksum=sha256:afb22a26e7d489d72db0a2305d433a3c3ac2692959ba2412011c0d759b4c8518&registry=https:%2F%2Fgitroot.dev%2Freleases%2F0.5.0
6  active: true

GitRoot will download $registry/$scope-$name-$version.wasm, so it will be https://gitroot.dev/releases/0.5.0/gitroot-ladybug-0.0.5.wasm for the example. And then compare the downloaded WASM binary with the checksum provided in the PURL.

Look in the plugins registry to find the exact purl for each plugin.

To know why, how and more read purl details.

A lot of people have discovered GitRoot

First show, first lobste.rs/HN links, first video… Yes, more and more people are talking about GitRoot. Discover where.

Not conclusive

Sometimes you spend more time on a subject than you thought. And sometimes, after all of this time spent, you don’t have what you wanted to have at the start… That is the case for the sdkgenerator experiment. It should “just” generate Rust, TS, Zig code from Golang… “Just”, lol, I never finished it. Maybe someone wants help? Read the full sdkgenerator story.

Before it’s too late

How to prevent spam with a forge without registration? This question was asked by NLnet 2 years ago (no, they don’t fund GitRoot, I was rejected). Two years later I have the answer, not perfect, but it’s a start. I added two new properties: one in .gitroot/forgeConf.yml of your root repository, NbMaxBranchesPerProjectBeforeRejectingAnonymous, which is the number of branches before rejecting anonymous pushes. And one in .gitroot/repoConfiguration.yml of all your repositories, NbMaxBranchesBeforeRejectingAnonymous, which serves the same purpose but for a single project. Read why and see the full example

My new favorite language…. until the next one

After Golang, AssemblyScript, and Rust, the plugin-sdk is now accessible for the Zig language. I continue to add new languages and discover how they work. This one was especially refreshing in its implementation. Read more

Plugins needs more power

Talking about the plugin-sdk, they all receive a lot of new features. They can now schedule jobs, make http requests, and configure plugins during CallFunc. I have also simplified the initialization part. Read details

Sftp to save the web

How to add images to the web without cluttering Git? That was the problem I had to tackle before adding slideshows to GitRoot. SFTP came to the rescue, and with ~200 lines of code, it’s now possible. I love when a simple, established protocol allows you to fix a complicated feature. Read more to know how to enable it.

How many bugs?

A lot of bug fixes have been done. Let me explain some interesting ones.

In-depth Details

PURL breaking details

At the beginning of this story, I only wanted to add a scope for plugins. That way, if you install a plugin with gitroot as the scope, you can think “it looks less dangerous” than anyother scope. Just like I suspect less @angular than some odd Node.js package (well, the npm ecosystem is maybe a bad example for that… but the only one right now).

So, my simple idea was to transform url: https://gitroot.dev/releases/0.4.0/ladybug-0.0.4.wasm into something like url: https://gitroot.dev/releases/0.4.0/gitroot-ladybug-0.0.4.wasm, so that a gitroot-*.wasm is affiliated with me. In the registry, I can show the scope.

Another example is if someone forks ladybug and wants to release it in the registry. Previously there was no option to do that, so adding a scope seemed to be the best choice.

During my research on a way to model this scope, I found the ECMA-427 Package-URL (PURL) specification. PURL stands for Package URL (I call them Plugin URL), a way to express many packages as a URL. What a perfect fit for the GitRoot plugin system!

This is how a PURL is represented: scheme:type/namespace/name@version?qualifiers#subpath. In GitRoot, I use only pkg:gitroot/$scope/$name@$version?checksum=$checksum&registry=$urlToDownload (maybe a default registry like https:%2F%2F%2Fgitroot.dev will be added in a next version, I’m not sure about that for now). There is no specification about what a registry should do to implement the GitRoot specification. For now, GitRoot will download $urlToDownload/$scope-$name-$version.wasm, and that’s all.

Thanks to PURL, I can delete a lot of other properties in the .gitroot/plugins.yml file, like version, checksum, and name. I also use it a lot in the code to refer to a plugin. Previously I only used the name, but now I use a full PURL. So, for example, if a plugin wants to call another plugin (thanks to the CallFunc API), it can define it as pkg:gitroot/$scope/$name (to mean all possible versions), or an exact version with pkg:gitroot/$scope/$name@$version. Adding a checksum and even a registry is also possible.

That was a big refactor to do, but I’m pretty happy with the result.

GitRoot communication

A lot has occured during this summer, on the communication side too.

I want to thank Adullact and particularly Matthieu for letting me present the first show at the Adullact congress. The video is only in French without subtitles: Souveraineté : Savez-vous vraiment qui contrôle vos logiciels ?. It was a great two-day congress, I talked to a lot of passionate people. And for once, they weren’t developers.

Chart with number of unique visitor, a uge pick (from 500 to 13k) is visible around end of jul start of aout.

Then the first appearance on lobste.rs and Hacker News occurred. Thanks hasnep for sharing, you can follow the initial exchange on my GoToSocial. As the image shows, there was a spike in the number of visitors, emails, and questions asked on the platforms and in private. That was a great moment to experience.

After the mentions on these platforms, some other cool articles popped up (I won’t link them as they are principally AI-generated slop). But there was also a video showing how to run GitRoot in a nanoVM: https://www.youtube.com/watch?v=g5nW9g5T4wU.

Another particular point of pride was the first mention by Andrew Nesbitt in the blog article Issues in the Repo. Thanks to Damien for talking about GitRoot to Andrew. Since you deleted the toot, I guess you don’t want to be cited here. If that’s not the case, contact me.

And GitRoot has been added to AlternativeTo. I don’t know if this will lead to anything, but hey, I’m right alongside GitHub and GitLab :P

SdkGenerator full story

This experiment comes from a bored developer who didn’t want to use LLMs, but also didn’t want to rewrite the same models/objects/structs (call that what you want) four times. When I want to add a new function to the plugins-sdk, I do it in one language, and then, when it works, I need to rewrite it in all the other languages (more on that in the “Modifying the API in the SDK Plugin” blog article).

In my day job, I love to build tools that generate code. I have written a tool that takes Kotlin as input and generates TypeScript output. Then I wrote a tool that generates complete Angular components from Kotlin enums (yeah, bored of writing a form select from an enum). And finally, it generates complex Angular services from Spring annotations (when you are very bored, a lot of things can be generated).

So it’s naturally that I started to write a tool that generates TS, Rust, and Zig from Go. But the experience is not conclusive… A lot of language specifics cannot easily be shared by a tool. I’m not giving up… I just have more interesting priorities (yes, I’m less bored on GitRoot than at my work ^^). But if this is the kind of thing you like, yes, you, reading that, please reach out to me, and I’ll explain all that I did and how you can take it further.

By the way, I discovered another kind of tool that does a similar job: WIT. It’s not an official spec from the W3C, but since it’s from the Bytecode Alliance, it’s big enough to use. I have quickly looked at it, and the engine I use for Wasm is not really compatible for now. But I’m keeping that in mind; it could be a replacement for SdkGenerator if nobody (including me) wants to do the job.

Max anonymous branches

When a lot of people started to talk about GitRoot, one thing haunted my nights: what if someone builds a simple script that clones GitRoot, creates 1,000 branches, and pushes them? I would need to git push origin :branch for every single one of them. You don’t need to register to contribute to GitRoot, and that’s one of its strengths, but also one of its weaknesses.

How to make that impossible without blocking all the beauty of GitRoot? One day, a simple solution came to me (yes, I love imagining scenarios running against me). Why not limit the number of branches from anonymous users? It’s simple after all: I need to count the number of branches made by anonymous users, and if it exceeds a limit, I stop accepting them. However, people I have added to my repo because of a previous contribution can still push new branches. That is what I added, but… there is always a but… counting the number of branches is lightweight, but counting the number of branches from an anonymous user is really expensive. Life is full of choices, and I made one.

When the branch limit is reached, we stop accepting anonymous contributions. That means if you set 10 as the maximum and you make 20 branches, nobody else can contribute. Yes, it’s not perfect, but it’s way better than having nightmares every night.

If an admin configures the forge with NbMaxBranchesPerProjectBeforeRejectingAnonymous=10, that means when a project has 10 branches (who added them doesn’t matter), the next anonymous user (not present in .gitroot/users.yml) will be rejected. Of course, all known users can continue to create as many branches as they want.

A project can set a lower number, NbMaxBranchesBeforeRejectingAnonymous=5, but if the number is bigger, it will have no effect (e.g., if set to NbMaxBranchesBeforeRejectingAnonymous=15, GitRoot will use 10 as set by the forge administrator).

A neat trick: setting NbMaxBranchesBeforeRejectingAnonymous=0 blocks all anonymous pushes. Alternatively, you can use the Not accepting anonymous contribution guide. The main difference is the error message: the first gives anonymous branches not allowed, while the latter shows you can't write in $branchName.

Zig plugin-sdk

With every release, I try to add a new plugin-sdk language. This time I picked zig because it is a language that natively manages Wasm and WASI, so I was pretty sure I would finish this implementation.

I loved the error system, compared to Rust (optionals) and Go (multiple returns), Zig chose an in-between solution with ! and some language keywords. But what totally blew my mind is the comptime feature, where the compiler can become an interpreter for compilation output. Amazing idea, I am pretty sure a lot of things will follow this path.

Long story short, I spent a long time discovering the language, and it was a very good time for me (it changes your perspective, seeing the same problem solved another way)… but nothing related directly to GitRoot.

As with all the plugin-sdks, there is unfortunately no way for you to use it yet. I don’t release them in registries until the API is stable, and a lot of breaking changes are coming in the next release.

PluginSdk improvements

Regarding the plugin-sdk, here are a lot of new capabilities for plugins to use:

schedule: you can allow a plugin to be called at a scheduled time. The format is a cron expression with 5 segments as <minute> <hour> <day> <month> <weekday>. Example:

1- purl: pkg:gitroot/gitroot/firefly@0.0.1?checksum=sha256:fae956f5cc9239a87b1c2a149a5466fec85476377de4ada33dda190edb3700c4&registry=https:%2F%2Fgitroot.dev%2Freleases%2F0.5.0
2  active: true
3  run:
4    - path: ""
5      branch:
6        - main
7      when: []
8      schedule: "* * * * *"

That means the firefly plugin will be executed on the main branch every minute. Refer to the gronx library documentation for cron expression details.

httpClient: plugins can make HTTP requests. I have added standard verbs (GET, PUT, POST and DELETE), if you need more, please let me know. You can also add headers as a map[string][]string. In the next version, I will add permission a plugin must request beforehand.

callFunc configuration: you can add a configuration for the called plugin within the caller plugin’s callFunc. This makes it possible to tell the called plugin what its configuration is in this context. Previously, an empty config was set, so the plugin’s defaults (if any) were used. Example:

1callFunc:
2  - pluginPURL: pkg:gitroot/gitroot/apex
3    funcName: renderHtml
4    configuration:
5      branchesDir: branches
6      favicon: logo.png
7      footer: <p>Start with <code>git clone ssh://gitroot.dev/</code> 🚀</p><small>Hosted with ❤️ by Gitroot</small>
8      generateGitWorktree: true
9      header: ...

init function, one to rule them all: I have refactored the init() function of the plugin-sdk to provide an initKind model.InitKind, which can be confhaschange, diff, schedule, or funccall. This lets the plugin know in which context it is running, and this architecture allows future hooks to be added without breaking the API.

pull-iterator: I have introduced pull-iterators to be able to group reports without sending all reports to the Wasm memory. In a future version, I will use this mechanism to transfer commits and files directly to Wasm.

Sftp feature

Users can access every repository’s web directory through SFTP. To do this, you can use the same SSH key that you use for your Git repository. When you log in with sftp -P $SshPort $SshAddress:/$repo/ (like sftp -P 4545 127.0.0.1:/repo1/), you will see the list of web representations for your repository. Anyone can do this, but only those in the “owner” section of .gitroot/users.yml have write access.

Be careful: this feature is in alpha, so don’t write scripts around it for now. I will probably change the directory hierarchy #d42a.

To enable this feature, you need to change activesftp: false to activesftp: true in .gitroot/forgeConfig.yml at the root repository of your instance. Don’t forget to restart GitRoot after that, otherwise the parameter will not be taken into account by GitRoot.

Some interesting bug fixes

Of course, a lot of bug fixes have been included in this release. I will present only the most interesting ones, and you can find a more complete list in the changelog and an even more thorough one in the commits.

The first annoying bug occurred when Grafter committed to a file, but Apex did not render that file if the Grafter plugin was placed after Apex in the configuration. In fact, even if you put Grafter before Apex, the bug persisted because Apex reports the rendering back to Grafter, meaning it missed that final report. Now, I have added a subsequent loop where plugins can only write to the web directory (and cache) but not to Git. This way, all plugins loop over commits and files to generate new commits, and only afterwards do we loop through all plugins to see if they have anything to write to the web (or cache) excluding Git to avoid an infinite loop.

Another annoying issue happened when you changed a plugin’s configuration, causing it to enter a worktree loop (looping over files in the worktree instead of commits). Consequently, the commits generated by this loop were not propagated to other plugins, causing widespread rendering misses on the web.

Finally, if you decided to add a plugin multiple times in your .gitroot/plugins.yml file (to have multiple configurations, for instance), only the first instance was actually loaded and executed. Now, you can include a plugin as many times as you want, with any configuration you choose, and they will all be executed.

What next?

New plugins, bug fixes, updates… Yes, until now this article was only about GitRoot, but what is GitRoot without its plugins? The immediate next thing I will do is write an article about what new plugin versions can do.

Then I will probably start working on:

If you compare this with the last v0.4 what next section, there are a lot more things. Every time I finish a feature (or don’t, see the plugin-sdk generator), I can add ten more new features to do. That’s why I love GitRoot, and that’s why I Move slowly, and take care of things.

Special thanks to Ryuno-Ki, Matthieu and ks from the #gitroot:matrix.org Matrix room for reviewing and correcting this article. Want to help with future posts? Join us in the room!