Gerrit forge integration
I'd like to see Gerrit being one of the supported forges, including pushing branches for review. I might be able to work on this, but first wanted to open up a discussion if this has been looked into, and what considerations I should take if looking at this. Thanks!
Thanks a lot for sharing, it's much appreciated!
This is the first issue about this topic, and I don't recall anything about it either, except that supporting Gerrit might be more involved as it requires its own form of a change-id? Pushing branches there might also be quite different, even though maybe in the end its just (force)pushing to one particular ref?
With all that said, my biggest concern would be the change-id, as the existing forge abstraction should be strong enough to adapt to Gerrit. Of course, there will be a lot that I am missing π .
A few notes after brainstorming with @krlvi
- Gerrit has the concept of pushing to normal refs (branches), just like any other git server, so we should not necessarily equate "pushing to Gerrit" as "create reviews". Just like GitHub/GitLab, those are in principle two steps: push, and create review
- The most natural mapping from GB's virtual branches are a set of individual changes in a Gerrit relation chain. We don't want to treat each gerrit change (commit) as a separate virtual branch in GB
- Gerrit changes can be tagged with a "topic", which would map well to the named virtual branch
- GB currently associates a review with a virtual branch, based on the GitHub/GitLab PR model, but in Gerrit reviews are per commit. Ideally we'd show a link/button in the UI for each of the commits, pointing to the associcated gerrit change, but as a stepping stone we can have a "review link" for the virtual branch that points to the gerrit topic, or the last change in the relation chain
- For pushing and pulling changes we probably want to base things off the Git protocol, like we do for other forges, but it may be too unreliable to try to create reviews using the
refs/for/foomagic ref, as the protocol doesn't (?) give us the resultingrefs/changes/123ref. As an alternative we should consider the Gerrit REST API, which hopefully allows creating changes from existing refs (?)
As someone that is quite involved with the Gerrit world, I thought I'd chime in here.
Gerrit changes can be tagged with a "topic", which would map well to the named virtual branch
I'm not an avid GitButler user, and not very familiar with virtual branches. If the aim here is to prevent the user from submitting individual commits of the virtual branch, without submitting the whole virtual branch, then I think this makes sense? Topics in Gerrit have a very specific funtion, to force changes(potentially from multiple repositories) to be submitted only when all changes in the topic are submittable, so we should use them only if we need this functionality.
If the aim is rather to just provide a pointer to the gerrit change, then maybe we should use hashtags, as they don't carry as strong of a meaning in Gerrit.
but it may be too unreliable to try to create reviews using the refs/for/foo magic ref, as the protocol doesn't (?) give us the resulting refs/changes/123 ref.
Yeah, there is a create change endpoint, but can't say that it gets much use. This is definitely me not being proficient enough in GitButler, but why would GitButler need to care of the name of the ref created?
Thanks for your insights @DanieleSassoli!
Topics in Gerrit have a very specific funtion, to force changes(potentially from multiple repositories) to be submitted only when all changes in the topic are submittable, so we should use them only if we need this functionality.
Interesting. That's now how I'm used to using them, but that might be because in Qt's Gerrit instance we don't use the regular submit-flow (we have our own submit that stages the change for CI integration, and a complex machinery of submodule updates that try to align things π ).
The docs describe topics as more of a way to collect related changes though. And according to these docs the behavior you're describing with multi-repo staging only applies if you've set config.submitWholeTopic, so it can probably be argued that topics can be used for other means as well.
In my mind hash-tags are more like labels, where you can have more than one on any given change, while the virtual branch names are more unique. But I guess in the end it doesn't matter that much as long as the virtual branch name is somehow reflected on the Gerrit side π
Yeah, there is a create change endpoint, but can't say that it gets much use.
I looked at it briefly, and sadly it looks like it can't be used to create a change from an existing branch, say refs/personal/<myusername>/<mytopicbranch>. You have to pass a textual diff. Is that the case?
but why would GitButler need to care of the name of the ref created?
Once the commit(s) have been pushed to Gerrit for review by GitButler it needs to know what upstream branch to track to know if its out of sync with the upstream. For example if someone does an edit of the change in the Web UI, creating a new patch set. Or pushing a new patch set outside of GitButler (which may easily happen if someone tweaks a change to fix a build issue to help move a patch along).
The docs describe topics as more of a way to collect related changes though. And according to these docs the behavior you're describing with multi-repo staging only applies if you've set config.submitWholeTopic, so it can probably be argued that topics can be used for other means as well.
This is true, but as there is no way of knowing if someone has that configuration set or no from the outside we should probably be conservative here, I think hashtags are probably more what we want for this scenario. Also, as far as I know, you can have only one topic per change, so if someone uses topics for their own workflow it would be annoying if GitButler already set one for them, you then loose the ability of using that for your own needs. As you mention, you can use multiple hashtags, so it won't matter if GitButler adds a hashtag to your change.
I looked at it briefly, and sadly it looks like it can't be used to create a change from an existing branch
No but I think it can be used to create a change from an existing patch, compatible with the output of git diff, agreed, this doesn't sound ideal as I'd also prefer to leave the push-ing at a git protocol level.
Once the commit(s) have been pushed to Gerrit for review by GitButler it needs to know what upstream branch to track to know if its out of sync with the upstream.
Of course, this makes sense, sorry. We could parse the output of git push origin HEAD:refs/for/branch-name, i.e.:
remote: Processing changes: refs: 1, new: 1, done
remote:
remote: SUCCESS
remote:
remote: http://gerrithub.io/c/my-test/+/5741 test [NEW]
remote:
This will have the change number, and from there we can derive the ref name refs/changes/yz/wxyz/meta, and we only need to keep track of the meta ref to know if locally we're in sync or not. Agreed, it's not in the git protocol, but I don't think the format fo the response by Gerrit is likely to change anytime soon.
One added bonus of Gerrit keeping all the metadata in the meta ref, is that GitButler would be able to know the state of the change as soon as it fetches, including wether it's been approved or not, and even track the evolution of the various labels locally if we wanted too, in a distant future.
Also, as far as I know, you can have only one topic per change, so if someone uses topics for their own workflow it would be annoying if GitButler already set one for them, you then loose the ability of using that for your own needs. As you mention, you can use multiple hashtags, so it won't matter if GitButler adds a hashtag to your change.
That's a very good point βΊοΈ
We could parse the output of git push origin HEAD:refs/for/branch-name
Yeah, that's the initial approach @krlvi and me discussed. It's good enough as a start to get this going, and we can alway improve on it later by teaching Gerrit a few new tricks on the REST or git-receive-pack side (e.g. a -o or custom-keyed-value mode that makes it spit out machine-readable JSON instead of the message?)
Some more thoughts π
Looking at the existing UI flow for pushing to to GitLab (I assume GitHub is the same), I see there's a both a "Push" and a "Create MR" button:
In the case of Gerrit, it would probably make sense to only show a "Create CRs" (change requests) button, since in most cases you can't push directly to the remote tracking branch, or even to a refs/personal/<me>/topic-branch either. We could look into adding direct push support later, but for a first iteration a "Create CRs" button would best I think.
Change requests plural because the button is currently on the virtual branch, which if there are multiple commits in that branch will result in multiple change requests on the gerrit side. Each change tagged with a hash, like @DanieleSassoli suggested, e.g. <gerrit-username>-<virtual_branch_name> or just the virtual branch name.
Unlike GitHub and GitLab, when pressing the button, there's not anything the user can add in terms of a global CR title/description:
But we could use that intermediate step to allow users to specify who to CC to the change, and possibly other push options described here.
Once a set of CRs have been pushed for the virtual branch we'd ideally track them per commit, so that we can show the CR ID next to each commit, instead of on the virtual brach like here:
If we manage to do that we don't need a "View CRs" button on the virtual branch, but I guess it could link to the hash-tag we used?
If more commits are added to the virtual branch, or edits/amends are made, or even squashes or removals, those should all be possible to reflect to gerrit by e.g. clicking the virtual-branch's "Push" button (which now is visible, since we have remote refs we know we can push to).
Ideally this would all map transparently, so that if e.g. a commit is dropped, it's abandoned on the gerrrit side. Same with a squashed commit, in which case one of the two changes on the gerrit side would be abandoned with a message referring to the remaining CR.
We also need to track the remote gerrit state of each CR via its refs/changes/ ref, and let the user pull in new changes.
We could look into adding direct push support later, but for a first iteration a "Create CRs" button would best I think.
Agreed, you could query endpoints to figure out user permissions and see what buttons to show based on that, but definitely not needed to begin with.
Change requests plural because the button is currently on the virtual branch, which if there are multiple commits in that branch will result in multiple change requests on the gerrit side.
From what I understand of GitButler, this makes sense to me. If at some point naming conventions between GitButler and Gerrit could converge that'd be amazing, but that's for another time.
If more commits are added to the virtual branch, or edits/amends are made, or even squashes or removals, those should all be possible to reflect to gerrit by e.g. clicking the virtual-branch's "Push" button (which now is visible, since we have remote refs we know we can push to).
Ideally this would all map transparently, so that if e.g. a commit is dropped, it's abandoned on the gerrrit side. Same with a squashed commit, in which case one of the two changes on the gerrit side would be abandoned with a message referring to the remaining CR.
Yes exactly, and I think this is where GitButler and Gerrit work so well together.
We also need to track the remote gerrit state of each CR via its refs/changes/ ref, and let the user pull in new changes.
Yeah, as mentioned earlier, you only need to track the meta ref per change to see if anything has changed, and if it has then fetch that set of refs yeah.
I guess there's a risk of adding a fair amount of load onto the Gerrit server depending on how aggressive GitButler is fetching, so the fetch interval should probably be configurable at some point.
One thing I've discussed with the GitButler guys separately, is pushing to different branches. @torarnv I'm not sure how much of a concern this is for you? Do you tend to always develop against the same branch or against different branches at the same time? I have a feeling this is very project depending, (like contributing for Gerrit core happens on many different branches at the same time). So it'd be cool if we could have multiple local branches, each of which is "for" a different remote branch. Again though, not something we need from the get-go for sure.
Anyway, this all sounds great!
We also need to track the remote gerrit state of each CR via its refs/changes/ ref, and let the user pull in new changes.
Yeah, as mentioned earlier, you only need to track the
metaref per change to see if anything has changed, and if it has then fetch that set of refs yeah. I guess there's a risk of adding a fair amount of load onto the Gerrit server depending on how aggressive GitButler is fetching, so the fetch interval should probably be configurable at some point.
Right, good point. I do believe GitButler already has a fetch interval config somewhere.
One thing I've discussed with the GitButler guys separately, is pushing to different branches. @torarnv I'm not sure how much of a concern this is for you? Do you tend to always develop against the same branch or against different branches at the same time? I have a feeling this is very project depending, (like contributing for Gerrit core happens on many different branches at the same time). So it'd be cool if we could have multiple local branches, each of which is "for" a different remote branch. Again though, not something we need from the get-go for sure.
This is definitely a concern for me as well. In Qt we mostly work on and target the dev branch (our main), and then pick changes automatically down to release branches based on a Pick-to footer we add to the change:
https://codereview.qt-project.org/c/qt/qtbase/+/671944
But it's also very common to work on and push changes for any of the release branches, say if you need to manually resolve a merge conflict in one of the picked changes, or push a hot-fix for a release that doesn't go via dev (or is forward-picked to dev).
That's why GitButler's (current) limitation of a single target branch feels a bit off to me, as outlined in https://github.com/gitbutlerapp/gitbutler/discussions/7435.
It's a different/wider discussion, so I'll file a separate issue about that, but it's not clear to me why GitButler needs to manage its upstream target branch in any special way, as git already has support for this via a branch's upstream tracking branch: git branch --set-upstream-to=origin/foo. Intuitively I would have expected GitButler to pick up and use git's definition of the upstream branch as its target for whatever branch you're currently working off of.
Which brings me to the second part of the mental disconnect for me, the (single, and special) gitbutler/workspace branch. If I'm working on dev, 6.9, and 6.8 in parallel, all checked out in separate work trees, and all with their corresponding upstream tracking branches set (which may not even come from the same remotes, e.g. public and private repos), I would have preferred to use those branches as my workspace, ie letting GitButler manage the octopus merge commit directly on those branches.
Anyways, I'll file separate issues for these before I ramble on too much π
Anyway, this all sounds great!
π
Guys, we just released 0.17.0 https://github.com/gitbutlerapp/gitbutler/releases/tag/release%2F0.17.0 which now has support for a bunch of Gerrit things.
Let's use this thread to discuss anything that is not yet good enough, needs fixing, changing etc
This is awesome @krlvi !!! Will test ASAP! β€οΈ
Some observations:
When pushing my changes to Gerrit, they are pushed correctly, but GB seems to fail to record this, so they are still sitting as "unpushed":
If I then try to re-push, the UI claims all is well:
But the console log shows:
2025-10-30T15:48:01.706997Z ERROR crates/gitbutler-git/src/executor/tokio/mod.rs:89: Git invocation failed cmd=Command { std: cd "/Users/torarne/dev/tmp/qtbase/.git" && env -u GITBUTLER_ASKPASS_PIPE -u GITBUTLER_ASKPASS_SECRET -u SSH_ASKPASS DISPLAY=":" GIT_SSH_COMMAND="\'/Applications/GitButler.app/Contents/MacOS/gitbutler-git-setsid\' ssh -o StrictHostKeyChecking=accept-new -o KbdInteractiveAuthentication=no" GIT_TERMINAL_PROMPT="0" LC_ALL="C" "git" "-c" "protocol.version=2" "--no-pager" "push" "--quiet" "--no-verify" "qt-project" "ee0f39b713b83a12665ad74f67da1ba944b71a08:refs/for/dev" "-o" "wip" "-o" "topic=hepp", kill_on_drop: true } stdout="" stderr="remote: \rremote: Processing changes: refs: 1 \rremote: Processing changes: refs: 1 \rremote: Processing changes: refs: 1, done \nTo ssh://codereview.qt-project.org:/qt/qtbase\n ! [remote rejected] ee0f39b713b83a12665ad74f67da1ba944b71a08 -> refs/for/dev (no new changes)\nerror: failed to push some refs to 'ssh://codereview.qt-project.org:/qt/qtbase'\n"
Because the remote reports "no new changes".
Thank you so much for testing this. I am trying to see if i can reproduce this situation - I am assuming that there were indeed changes in those commits
Yepp, they are all visible on the Gerrit web UI
Thank you this is very helpful! Would you mind sharing the output of a manual commit push to refs/for/<target>.
I am particularly interested in the stderr that comes from the server - it seems like the parsing the app does is incorrect
This parse function in particular may be incorrect for the output that you get https://github.com/gitbutlerapp/gitbutler/blob/master/crates/but-gerrit/src/parse.rs#L40
β― git show
commit d0d4fb8105b557d2b2c710a2f01bdd2f22b99331 (HEAD -> dev)
Author: Tor Arne VestbΓΈ <[email protected]>
Date: Thu Oct 30 17:56:06 2025 +0100
Test manual push
Change-Id: Ie7934d186a9c0e02bf16ccbcccfbe4844c22fef9
config_help.txt:1:
Usage: configure [options] [-- cmake-options]
Test test
This is a convenience script for configuring Qt with CMake.
Options after the double dash are directly passed to CMake.
You can pass CMake variables as configure arguments:
β― git push qt-project HEAD:refs/for/dev%private
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 355 bytes | 355.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2)
remote: Processing changes: refs: 1, new: 1, done
remote:
remote: SUCCESS
remote:
remote: https://codereview.qt-project.org/c/qt/qtbase/+/687666 Test manual push [PRIVATE] [NEW]
remote:
To ssh://codereview.qt-project.org:/qt/qtbase
* [new reference] HEAD -> refs/for/dev%private
Confirmed that pushing without private works:
Oh I see! Thank, you. Fixing the parsing right away
Filed https://github.com/gitbutlerapp/gitbutler/issues/10908 for the gerrit URL detection (using push, not fetch).
Does the sync handle upstream changes in the Gerrit remote? Ie someone editing a commit message or the change/files itself, creating another patch-set? I guess in that case the local state needs to signal that somehow, and warn that you need to pull down the modified change, or force-push yours over it?
What determines whether the Gerrit push UI is shown? If I set gitbutler.gerritMode true in my global .gitconfig, GB correctly pushes to refs/for, and shows the CR label:
But doesn't show the push options UI.
As far as I know, gitbutler.gerritMode = true is the only flag to determine whether Gerrit support is enabled or not.
If it doesn't show up consistently, this might be a bug.
CC @krlvi .
Does the sync handle upstream changes in the Gerrit remote? Ie someone editing a commit message or the change/files itself, creating another patch-set? I guess in that case the local state needs to signal that somehow, and warn that you need to pull down the modified change, or force-push yours over it?
Related to this, I tried opening GB in a repo where I already have some commits that are pushed to Gerrit. It would be awesome if GB could detect their state and CR identifier (adding a label) based on the Change-Id footer, either via SSH or the REST API.
Here's another weird one: I pushed two commits in a virtual branch, and only the last one got a CR label, although both were pushed to Gerrit:
Small issue with how the Change-Id footer is added:
https://codereview.qt-project.org/c/qt/qtbase/+/688097/1//COMMIT_MSG
It doesn't take into account existing footers, always adding a newline. Would be nice if it detected that there's a footer section already, and amending that.
Here's another weird one: I pushed two commits in a virtual branch, and only the last one got a CR label, although both were pushed to Gerrit:
![]()
This one seems to be because the change is exactly what's upstream, which GB currently doesn't detect/sync with:
2025-11-01T15:30:04.750803Z ERROR crates/gitbutler-git/src/executor/tokio/mod.rs:89: Git invocation failed cmd=Command { std: cd "/Users/torarne/dev/qt/.git/modules/qtbase/worktrees/qtbase" && env -u GITBUTLER_ASKPASS_PIPE -u GITBUTLER_ASKPASS_SECRET -u SSH_ASKPASS DISPLAY=":" GIT_SSH_COMMAND="\'/Applications/GitButler.app/Contents/MacOS/gitbutler-git-setsid\' ssh -o StrictHostKeyChecking=accept-new -o KbdInteractiveAuthentication=no" GIT_TERMINAL_PROMPT="0" LC_ALL="C" "git" "-c" "protocol.version=2" "--no-pager" "push" "--quiet" "--no-verify" "qt-project" "24966f126d68326a4167a64350597cb7eec20209:refs/for/dev", kill_on_drop: true } stdout="" stderr="remote: \rremote: Processing changes: refs: 1 \rremote: Processing changes: refs: 1 \rremote: Processing changes: refs: 1, done \nTo ssh://codereview.qt-project.org/qt/qtbase.git\n ! [remote rejected] 24966f126d68326a4167a64350597cb7eec20209 -> refs/for/dev (no new changes)\nerror: failed to push some refs to 'ssh://codereview.qt-project.org/qt/qtbase.git'\n"
Small issue with how the Change-Id footer is added:
https://codereview.qt-project.org/c/qt/qtbase/+/688097/1//COMMIT_MSG
It doesn't take into account existing footers, always adding a newline. Would be nice if it detected that there's a footer section already, and amending that.
@torarnv I think this PR https://github.com/gitbutlerapp/gitbutler/pull/10953 should take care of this - thanks for the repro/ test case
What determines whether the Gerrit push UI is shown? If I set
gitbutler.gerritMode truein my global.gitconfig, GB correctly pushes torefs/for, and shows the CR label:But doesn't show the push options UI.
Technically, the same property that determines if the review tag is rendered should be also used to determine if the push options modal is shown... but there could be a bug, taking a look
