Andrew Oberstar

Don't commit to grgit

April 2, 2024

TL;DR Before you start or continue using grgit, consider whether you really need it. Can you get by with tasks that call out to the git CLI directly, or write one-off tasks using JGit directly? The time will come (but hasn't just yet) when I stop maintenance.

While I first started drafting this post a few months ago, the xz backdoor was good motivation to finish it as the topics of maintainer burnout are quite relevant.

History

In 2012, Peter Ledbrook posted on the Gradle mailing list (which seems lost to history) offering up some code for Gradle tasks using JGit to publish to GitHub pages to anyone interested in making a plugin from them. Since I was young and eager, I took him up on it and created the gradle-git plugin originally focused on just publishing to GitHub pages, gradually building out more tasks to do common Git actions.

This was near the peak of Groovy's popularity, when Java was perceived as progressing unbearably slow. Groovy was my first exposure to functional programming concepts and having Groovy-optimized wrappers around clunky old Java libraries like JGit was appealing.

After 3 years of focusing on the more Gradle-native route, I chose to extract the generic Git support out into it's own Groovy wrapper for JGit, grgit. This meant that all of the functionality could now be used as a library in any Groovy program, while still primarily focused on supporting Gradle. I'm sure this was partly motivated by how massively terrible the experience of testing Gradle code is.

The gradle-git plugin continued to support deployment to GitHub pages and added in support for managing version numbers. Eventually it made sense to break the independent functionality apart, resulting in the current set of three repositories:

Maintenance Hurdles

Over time, continuing maintenance became a chore.

Gradle's breaking changes

Gradle has continually evolved its API, sometimes for reasons that seemed obviously beneficial, but often in ways that have been frustrating. Often new APIs touted as important replacements for existing ones are coupled with near immediate deprecation warnings that all of your users receive in their build output, leading to tickets asking for you to resolve them ASAP. Many of these new APIs don't stabilize for years, but a vocal portion of the user base wants them (e.g. configuration cache). Those often led me to adopt those APIs, when even many of Gradle's internals haven't bothered to switch, only to get burned when Gradle changes their mind about the right way for you to use them.

The vast majority of maintenance work in the last 5 years has just been keeping up with Gradle's moving targets, with no energy left to enhance the plugins themselves.

Mindshare moving to Kotlin

As time went on, Groovy's momentum stalled and ultimately dried up, with Gradle being one of the only reasons people still use Groovy. Groovy's decline coupled with Kotlin's growth drove Gradle to add support for Kotlin scripting.

For someone who was a pre-1.0 Gradle user and plugin author who had fully bought into Groovy at the time, many of my plugins were optimized for how Groovy works. Even though the plugin's tagline was "the Groovy way to use Git", users began to complain of difficulties using it with Gradle's Kotlin DSL.

This ultimately led me to rewrite my plugins (except grgit) in Java to take advantage of the more stable experience it provides and make them more obviously interoperable with Kotlin. This is more of an existential issue for grgit, due to it being a Groovy-specific wrapper (more on that later).

Limitations of Wrapping

Being a wrapper library brings two key problems:

  1. You can't easily support anything JGit doesn't support. Many times people asked for features that were available in the git CLI, but not in JGit. I would mark these as upstream:jgit issues in the tracker and just let them sit until someone pointed out that JGit added support (which rarely happened).
  2. You have to explicity wrap everything from JGit that you want. People would also ask for things that JGit could do, but I just hadn't had a reason to use from Gradle. Often, I wasn't all that interested in adding these either.

Motivation

I eventually had the deflating realization that I was on a hamster wheel of my own making. My motivation to work on these plugins was circular, mostly to make publishing my plugins easier.

For a while I fully ignored the plugins, only coming back when a mistake I had made in my dependency declarations caused user's builds to break when JGit released a version that dropped support for Java 8. I've since tried to make some updates 1-2 times a year.

Other Maintainers

Some might ask why I didn't try to bring in others to maintain the plugin. As the xz issue has shown, it's not that simple to get another maintainer that you trust. It's not that I thought some state-level actor was going to hijack my plugin, but I did feel uncomfortable with the idea of other people controlling plugins that use my domain name (and my real name) in their ID and package names. I think the sense of ownership, responsibility, or reputation is a common hurdle to bringing in others with full commit/release rights.

I'm sure there would have been people motivated to do be involved over the years, especially when Gradle/Groovy were more popular, but I don't know how much fervor there would still be. And at some point there's more value in having a stable plugin that people can keep using than making broader leaps forward that probably are more suited to their own from scratch plugins.

Possible Futures

As I've thought over the years of how to modernize grgit given the move to Kotlin, it hasn't just been motivation blocking me, but the question of why do you even need grgit? Is it even solving a problem that needs solving anymore?

Rewrite in Kotlin

One user had asked if it could be rewritten in Kotlin, I didn't bite on it and the user chose to do this themselves in a quasi-fork. I don't know how well that worked out for them, but the only real reason to rewrite in Kotlin is either familiarity with the language (which I don't have) or some sense of it having a better fit with the Kotlin DSL Gradle provides. I can't imagine it managed to preserve compatibility.

When Groovy was popular a lot of the sales pitch was around syntax sugar: basically just being able to write Java easier. By the time Kotlin surpassed Groovy, Java had improved enough that I no longer saw the benefit. I certainly wasn't going to spend time learning Kotlin, when I had by that point gotten more interested in Clojure and other Lisps.

Rewrite in Java

Another idea was to take the simpler interface that grgit provided and rewrite that in Java, still being a wrapper around JGit.

  1. That would still be a lot of work.
  2. Why are you wrapping a Java library in more Java? It just seems pointless.
  3. Java doesn't provide named argument support, which was one of the key benefits of grgit's interface.

Just use JGit

Another option is to abandon the plugin and just tell people they should use JGit directly instead. The main arguments in favor of this:

  1. Putting logic directly in your build.gradle is seen as bad form. Instead it's better to put logic in a plugin or buildSrc. In a plugin, the verbosity of raw JGit is not as much of an issue, and we're usually talking about a few lines difference. That also removes the need to match the language of Gradle's DSLs. There are a few pain points around error handling, since some actions require you to explicitly check the return value to determine if it was successful.
  2. You can do anything JGit lets you do, you won't be limited by what my wrapper exposes.
  3. You cut out a dependency, which in this day and age saves you a lot of hassle (especially if you work at a company with strict vulnerability management standards for 3rd party dependencies).

The main thing that you lose out on with raw JGit is authentication management. JGit requires any TransportCommand to pass along credentials. It's a non-trivial (but not terrible) amount of code to handle this consistently.

Just use Git CLI

Gradle can shell out to the Git CLI and run any command with any options Git supports. For a lot of commands like clone/push/tag, that's probably easier than using JGit.

What now?

My opinion is that grgit no longer serves a useful purpose in the Gradle ecosystem, given the evolution towards Kotlin DSL and the stronger preference to move build logic into plugins.

However, I understand the backwards compatibility need, so I'm not immediately stopping maintenance. I do consider the plugin feature frozen, and don't anticipate doing any work outside of updating dependencies and limited work to maintain compatibility with new versions of Gradle.

For my own sake, I've already decoupled reckon (which I don't plan to deprecate/archive) from grgit. I anticipate doing the same with gradle-git-publish, but I haven't had the time yet.