Git-gpush-scripts: Difference between revisions

From Qt Wiki
Jump to navigation Jump to search
(somewhat major rework; list more features)
(→‎If Something Goes Wrong ...: give better command for creating a bundle - include all non-upstream branch state)
 
(25 intermediate revisions by 5 users not shown)
Line 1: Line 1:
= The git-gpush Script Suite =
{{toclimit|limit=2}}
 
This page contains some background information for the git-gpush, git-gpick, git-gpull, and git-ggc scripts found in the [https://code.qt.io/cgit/qt/qtrepotools.git/ qt/qtrepotools repository].
This page contains some background information for the git-gpush, git-gpick, git-gpull, and git-ggc scripts found in the [https://code.qt.io/cgit/qt/qtrepotools.git/ qt/qtrepotools repository].


== Overview ==
Note: there is no need to install anything - just clone the repository (qt5's init-repository does that for you) and add its bin/ sub-directory to your $PATH.
 
Your system must have perl installed, and you may need to install some additional modules (e.g., Term::ReadKey and JSON), which can easily be done with [https://www.cpan.org/modules/INSTALL.html cpan], or preferably your distro's package manager. You do NOT need to install anything on Windows - Git for Windows comes with perl, and extending PATH per the above will make calls like <tt>git gpush</tt> (without a dash) just work.
 
=Overview=


The general idea of these scripts is to make working with Gerrit more transparent:
The general idea of these scripts is to make working with Gerrit more transparent:
* You can pull as often as you want, including right before pushing, as you would usually do in a <tt>git pull --rebase</tt> based workflow. Without git-gpush, this would unnecessarily rebase previously pushed Changes, which would make Gerrit's inter-diff feature slower and much noisier.
 
* You can keep many unrelated Changes locally without creating spurious dependencies between them on Gerrit. While you can achieve the same by having a separate local branch for every series, having all Changes in a single branch is much more convenient.
*You can keep many unrelated Changes on one local branch without creating spurious dependencies between them on Gerrit. While you can achieve the same by having a separate local branch for every series, having all Changes on a single branch is much more convenient. This is sometimes called "stacked branches".
* You can push updates to specific Changes without invalidating reviews of unrelated Changes.
*You can push updates to specific Changes without causing churn in unrelated Changes.
*You can pull as often as you want, including right before pushing, as you would usually do in a <tt>git pull --rebase</tt> based workflow. Without git-gpush, this would unnecessarily rebase previously pushed Changes, which would make Gerrit's inter-diff feature slower and much noisier.


You may have noticed that these major features are geared towards making not ''your'' life easier, but that of your ''reviewers''. Until roles are switched, that is.
You may have noticed that these major features are geared towards making not ''your'' life easier, but that of your ''reviewers''. Until roles are switched, that is.
Line 13: Line 19:
The way how these scripts achieve this is by letting you manage "virtual branches" (called "series") within your local working branch. When a series is pushed, its commits are rebased (usually to a commit from the upstream branch) to isolate it from other local Changes. The working tree is not affected by this in any way, so no unnecessary rebuilds result from this.
The way how these scripts achieve this is by letting you manage "virtual branches" (called "series") within your local working branch. When a series is pushed, its commits are rebased (usually to a commit from the upstream branch) to isolate it from other local Changes. The working tree is not affected by this in any way, so no unnecessary rebuilds result from this.


== More Features ==
=More Features=


=== git gpush ===
==git gpush==


* Updates to the Changes pushed from other clones of the target repository (or made directly on Gerrit) are detected, so you won't accidentally overwrite them.
*Updates to the Changes pushed from other clones of the target repository (or made directly on Gerrit) are detected, so you won't accidentally overwrite them.
* The target branch of each series is remembered, server-side moves are tracked, and attempts to create duplicates by pushing for the wrong branch are rejected by default.
*The target branch of each series is remembered, server-side moves are tracked, and attempts to create duplicates by pushing for the wrong branch are rejected by default.
* The scripts remember which Changes belong to a series, so once you pushed it, the complete series can be identified by specifying a single Change (or none at all, if it's HEAD). This saves you from needing to keep track of how many Changes are in the series. You don't even need to use the currently last Change in the series. Also, commits can be specified by Change-Id. Taken together, these features make it possible to use the same command line to repeatedly re-push the same series regardless of any local history rewriting.
*The scripts remember which Changes belong to a series, so once you pushed it, the complete series can be identified by specifying a single Change (or none at all, if it's HEAD). This saves you from needing to keep track of how many Changes are in the series. You don't even need to use the currently last Change in the series. Also, commits can be specified by Change-Id. Taken together, these features make it possible to use the same command line to repeatedly re-push the same series regardless of any local history rewriting.
* Or you can just push out all local series in one go.
*Or you can just push out all local series in one go.
* An overview of the pushed Changes and their status relative to Gerrit is displayed.
*An overview of the pushed Changes and their status relative to Gerrit is displayed.
* Pushes which would be rejected by Gerrit are detected upfront.
*Pushes which would be rejected by Gerrit are detected upfront.
* Unsquashed fixup commits and merges of non-upstream commits are rejected.
*Unsquashed fixup commits and merges of non-upstream commits are rejected.
* Changes duplicated between local branches are detected, and an authoritative source branch for actual pushes can be set.
*Changes duplicated between local branches are detected, and an authoritative source branch for actual pushes can be set.
* Reviewers/CCs can be conveniently added by their IRC nickname.
*Reviewers/CCs can be conveniently added by their IRC nickname.
*gpush can also be used without Gerrit at all, to feed a patch/e-mail based review workflow.


=== git gpick ===
==git gpick==


This script allows you to download pending Changes into your local branch, for local testing, reviewing and amending.
This script allows you to download pending Changes into your local branch, for local testing, reviewing and amending.


* Notable differences from Gerrit's built-in copy&paste-able download command lines:
*Notable differences from Gerrit's built-in copy&paste-able download command lines:
** Unlike 'cherry-pick', it can download an entire series in one go, so you just need to copy the Change-Id of the last Change and execute a single short command.
**Unlike 'cherry-pick', it can download an entire series in one go, so you just need to copy the Change-Id of the last Change and execute a single short command.
** Unlike 'checkout', your local Changes are not "hidden" on another branch, so you don't need to "switch contexts" (rebuild lots of stuff, deal with bugs you already fixed, etc.).
**Unlike 'checkout', your local Changes are not "hidden" on another branch, so you don't need to "switch contexts" (rebuild lots of stuff, deal with bugs you already fixed, etc.).
** Unlike 'pull', no merge commit is introduced, so local history rewriting remains unproblematic.
**Unlike 'pull', no merge commit is introduced, so local history rewriting remains unproblematic.
* The downloaded Changes are a separate series, so there is no danger that you accidentally re-push them together with other Changes.
*The downloaded Changes are a separate series, so there is no danger that you accidentally re-push them together with other Changes.
* Changes which already exist locally are replaced, so it is possible to pull in updates which have been pushed from other clones (or done directly on Gerrit).
*Changes which already exist locally are replaced, so it is possible to pull in updates which have been pushed from other clones (or done directly on Gerrit).
** This checks for conflicts, so you won't lose concurrent local modifications.
**This checks for conflicts, so you won't lose concurrent local modifications.
** Merging of non-overlapping concurrent modifications is possible.
**Merging of non-overlapping concurrent modifications is possible.
** It's also possible to apply only certain parts of the updated Changes, e.g., the commit message.
**It's also possible to apply only certain parts of the updated Changes, e.g., the commit message.
** There also exists the <tt>--check</tt> mode which only shows the updates on Gerrit without replacing any local commits.
**There also exists the <tt>--check</tt> mode which only shows the updates on Gerrit without replacing any local commits.
*** This being a simple 3-way diff, it also displays local modifications with more granularity than git-gpush does.
***This being a simple 3-way diff, it also displays local modifications with more granularity than git-gpush does.


== Getting Started ==
=Getting Started=


As usual, you should start with some solid RTFM: ;)
As usual, you should start with some solid RTFM: ;)
<code>
<syntaxhighlight lang="shell">
$ git-gpush -h
$ git gpush -h
$ git-gpick -h
$ git gpick -h
$ git-gpull -h
$ git gpull -h
$ git-ggc -h
$ git ggc -h
</code>
</syntaxhighlight>
 
(<tt>git help gpush</tt>, etc. do not work, because that would require installing man pages, which cannot be done for in-checkout execution).


Get all your pending changes onto the current branch, in case you kept them on separate branches:
Get all your pending changes onto the current branch, in case you kept them on separate branches:
<code>
 
<syntaxhighlight lang="shell">
$ git cherry-pick <…>
$ git cherry-pick <…>
</code>
</syntaxhighlight>


Alternatively, you could merge all your local branches and then rebase.
Alternatively, you could merge all your local branches and then rebase.


If you have pending Changes for the given branch on Gerrit, you need to synchronize the state:
If you have pending Changes for the given branch on Gerrit, you need to synchronize (bootstrap) the state:
<code>
<syntaxhighlight lang="shell">
$ git gpick --check --all
$ git gpick --check --all
</code>
</syntaxhighlight>


To get an idea about what gpush thinks you have in your branch:
To get an idea about what gpush thinks you have in your branch:
<code>
<syntaxhighlight lang="shell">
$ git gpush --list
$ git gpush --list
</code>
</syntaxhighlight>


Group remaining loose Changes into series:
Group remaining loose Changes into series:
<code>
<syntaxhighlight lang="shell">
$ git gpush --group :3
$ git gpush --group :3
$ git gpush --group ~3:5
$ git gpush --group ~3:5
<…>
<…>
</code>
</syntaxhighlight>
 
Group all Changes in the current branch into one series:
<syntaxhighlight lang="shell">
$ git gpush --group ..
</syntaxhighlight>
 
Get a more thorough overview:
<syntaxhighlight lang="shell">
$ git gpush --list-online
</syntaxhighlight>


Push out any modified Changes:
Push out any modified Changes:
<code>
<syntaxhighlight lang="shell">
$ git gpush --all
$ git gpush --all
</code>
</syntaxhighlight>


== Examples ==
Push out only the current commit:
<syntaxhighlight lang="shell">
$ git gpush :1
</syntaxhighlight>
 
=If Something Goes Wrong ...=
 
*If you get something that seems incomprehensible: slow down, think about it, ''don't'' use <tt>--force-*</tt> options right away
*Do ''not'' fall back to manual pushing/picking, either - the scripts offer a solution for almost every situation
**If you do after all (because you hit a bug or a documented limitation), always re-sync the state right afterwards
*If you get a crash or other behavior that cannot be reasonably explained, make a bug report:
**Make a JIRA task against QTQAINFRA/Git, and make sure it is assigned to me (Oswald Buddenhagen), or contact me directly via Matrix/IRC (ossi)
**Add the recent shell history including output, especially the backtrace if you get one, obviously
***If the problem is reproducible, re-run the failing command with <tt>--debug</tt>
**Add a snapshot of the git/gpush state, created with:<br /><tt>git bundle create gpush-state.bundle $(git for-each-ref --format='%(refname)' refs/gpush/ refs/heads/) --not $(git for-each-ref --format='%(refname)' refs/remotes/origin/)</tt>
*If you manage to get the scripts so confused that none of the documented overrides help, you can throw away the complete state with:<br /><tt>git update-ref -d refs/gpush/state</tt><br />note that it is very unlikely that you actually need to do that; and before you do, please save a snapshot and make a bug report
*If you get errors: ''Can't locate Term/ReadKey.pm in @INC (you may need to install the Term::ReadKey module)''  ''Can't locate JSON.pm in @INC (you may need to install the JSON module)''  then in Debian/Ubuntu you should do  ''sudo apt-get install -y libterm-readkey-perl libjson-perl''
 
=Examples=


To create a series consisting of two Changes, and push it, adding two reviewers:
To create a series consisting of two Changes, and push it, adding two reviewers:
<code>
<syntaxhighlight lang="shell">
<hack hack hack>
<hack hack hack>
$ git commit -a
$ git commit -a
Line 91: Line 129:
$ git commit -a
$ git commit -a
$ git gpush :2 +someguy +anotherguy@example.com
$ git gpush :2 +someguy +anotherguy@example.com
</code>
</syntaxhighlight>


Amend and re-push the series:
Amend and re-push the series:
<code>
<syntaxhighlight lang="shell">
$ git rebase -i @{u}
$ git rebase -i @{u}
<…>
<…>
$ git gpush
$ git gpush
</code>
</syntaxhighlight>


Add another Change to the series:
Add another Change to the series:
<code>
<syntaxhighlight lang="shell">
<hack hack hack>
<hack hack hack>
$ git commit -a
$ git commit -a
$ git gpush -e
$ git gpush -e
</code>
</syntaxhighlight>


Re-push the series when it's not at the top any more:
Re-push the series when it's not at the top any more:
<code>
<syntaxhighlight lang="shell">
$ git gpush I123415affe
$ git gpush I123415affe
</code>
</syntaxhighlight>


Download somebody else's series:
Download somebody else's series:
<code>
<syntaxhighlight lang="shell">
$ git gpick +Ie3f7567639cc05e03257cd5bac133714e5c4f068
$ git gpick +Ie3f7567639cc05e03257cd5bac133714e5c4f068
</code>
</syntaxhighlight>


No need to worry about accidentally re-pushing it any more!
No need to worry about accidentally re-pushing it any more!


Update your local copy after somebody has been messing with your Change (gpush will tell you about it, so you won't overwrite the Changes accidentally):
Update your local copy after somebody has been messing with your Change (gpush will tell you about it, so you won't overwrite the Changes accidentally):
<code>
<syntaxhighlight lang="shell">
$ git gpick I9ce6d9e7a3
$ git gpick I9ce6d9e7a3
</code>
</syntaxhighlight>


pull/rebase your local series:
pull/rebase your local series:
<code>
<syntaxhighlight lang="shell">
$ git gpull
$ git gpull
</code>
</syntaxhighlight>


== Answering Criticism ==
=Answering Criticism=


==== Shouldn't this be implemented by the server instead of client-side scripts? ====
===Shouldn't this be implemented by the server instead of client-side scripts?===


Some of the problems could be addressed on the server side, but others are inherently client-side:
Some of the problems could be addressed on the server side, but others are inherently client-side:
* The automatic "downbasing" to keep a Change's base constant even after it has been rebased locally could be done on the server, indeed.
* The management of series could - in principle - be done on the server. However:
** This requires additional -o options which would need to be passed to git push. Nobody wants to do that by hand, so you'd end up with a script ''anyway''.
** This would upload unrelated Changes just to discard them immediately.
* The status report can be done by the server - in fact, it already is done, though without giving per-Change information. However, for obvious reasons this happens only ''after'' uploading the Changes, which means you lose the potential for entirely avoiding doomed uploads.
* The concurrent update overwrite protection is inherently client-side, as only the particular clone can know what was pushed from it the last time.
* The reviewer add function as such is easy enough to use via -o options, but the irc alias lookup isn't.
* Identifying commits by Change-Id must be done client-side, as otherwise git doesn't even know what to push to the server.
* git-gpick is rather obviously a client-side tool.


==== This changes my workflow! This is unacceptable! ====
*The automatic "downbasing" to keep a Change's base constant even after it has been rebased locally could be done on the server, indeed.
*The management of series could - in principle - be done on the server. However:
**This requires additional -o options which would need to be passed to git push. Nobody wants to do that by hand, so you'd end up with a script ''anyway''.
**This would upload unrelated Changes just to discard them immediately.
*The status report can be done by the server - in fact, it already is done, though without giving per-Change information. However, for obvious reasons this happens only ''after'' uploading the Changes, which means you lose the potential for entirely avoiding doomed uploads.
*The protection against overwriting concurrent updates is inherently client-side, as only the particular clone can know what was pushed from it the last time.
*The reviewer add function as such is easy enough to use via -o options, but the IRC alias lookup isn't.
*Identifying commits by Change-Id must be done client-side, as otherwise git doesn't even know what to push to the server.
*git-gpick is rather obviously a client-side tool.
 
===This changes my workflow! This is unacceptable!===


If you refuse to take advantage of the automated series management gpush offers, the only necessary change in workflow would be consistently using <tt>git gpush ..</tt> (plus <tt>--rebase</tt> after conflicted pulls/rebases) instead of <tt>git push gerrit HEAD:refs/for/foo</tt>. If you are convinced that this is unacceptable, you should probably rethink your priorities.
If you refuse to take advantage of the automated series management gpush offers, the only necessary change in workflow would be consistently using <tt>git gpush ..</tt> (plus <tt>--rebase</tt> after conflicted pulls/rebases) instead of <tt>git push gerrit HEAD:refs/for/foo</tt>. If you are convinced that this is unacceptable, you should probably rethink your priorities.


==== It's a new tool! It's harder for newbies! ====
===It's a new tool! It's harder for newbies!===


Pushing to Gerrit already requires installing the Change-Id-creating commit-msg hook. Just cloning another (small) repository to get access to the scripts doesn't seem terribly hard in comparison. Also, qt5's init-repository script will clone the repository by default anyway, so the only thing necessary is adding the bin directory to git's PATH.
Pushing to Gerrit already requires installing the Change-Id-creating commit-msg hook. Just cloning another (small) repository to get access to the scripts doesn't seem terribly hard in comparison. Also, qt5's init-repository script will clone the repository by default anyway, so the only thing necessary is adding the bin directory to git's PATH.
And the basic usage of gpush is unarguably ''easier'' than that of plain git.
And the basic usage of gpush is unarguably ''easier'' than that of plain git.


==== It makes Qt different from other Gerrit-using projects! ====
===It makes Qt different from other Gerrit-using projects!===


These scrips are in no way specific to Qt. Anyone can use them with any Gerrit installation. Upstreaming will be considered at some point.
These scrips are in no way specific to Qt. Anyone can use them with any Gerrit installation. Upstreaming will be considered at some point.


==== I have my own scripts/aliases! ====
===I have my own scripts/aliases!===


Good for you! But switching will be well worth it, even if you have to retrain your finger memory. Promise!
Good for you! But switching will be well worth it, even if you have to retrain your finger memory. Promise!
Line 163: Line 203:
FWIW, I have aliased git-gpush to ggp, git-gpick to ggcp, and git-gpull to ggl.
FWIW, I have aliased git-gpush to ggp, git-gpick to ggcp, and git-gpull to ggl.


== Advanced topics ==
=Advanced topics=


=== Series Spanning Multiple Branches ===
==Series Spanning Multiple Branches==


It is sometimes necessary to fix bugs before adding a new feature. The bugfixes should typically target a stable branch, while the feature is for dev.
It is sometimes necessary to fix bugs before adding a new feature. The bugfixes should typically target a stable branch, while the feature is for dev.
gpush supports this scenario, but not optimally - Changes within the same series cannot have different attributes (like different target branches). While this would be supportable to a certain degree, the user interface gets really messy quickly.
gpush supports this scenario, but not optimally - Changes within the same series cannot have different attributes (like different target branches). While this would be supportable to a certain degree, the user interface gets really messy quickly.
Therefore, you have essentially two options, each with its own caveats:
Therefore, you have essentially two options, each with its own caveats:
# Target a single branch and move some Changes right before final approval and integration.
#* This confuses reviewers, so definitely requires additional communication on Gerrit.
#* It's better to target the stable branch first, because these Changes need to be integrated first. Otherwise you'd need to move them back and forth if they require amending.
#** You can do that just fine while working on dev locally, provided there are no merge conflicts.
#* gpush will adjust to the re-targeting and the already integrated Changes without requiring extra work or producing churn.
# You can designate two separate series in gpush, basing the feature series on the pending bugfix series by using the --onto switch.
#* You need to push the series one after another each time, unless you use --all mode.
#* Each series' base is remembered as an actual SHA1. That means that you need to manually adjust the 2nd series' base once it conflicts with earlier versions of the 1st series.
#* Solutions for both issues are planned, but not implemented yet.


=== Pushing Multiple Series at Once ===
#Target a single branch and move some Changes right before final approval and integration.
#*This confuses reviewers, so definitely requires additional communication on Gerrit.
#*It's better to target the stable branch first, because these Changes need to be integrated first. Otherwise you'd need to move them back and forth if they require amending.
#**You can do that just fine while working on dev locally, provided there are no merge conflicts.
#*gpush will adjust to the re-targeting and the already integrated Changes without requiring extra work or producing churn.
#You can designate two separate series in gpush, basing the feature series on the pending bugfix series by using the --onto switch.
#*You need to push the series one after another each time, unless you use --all mode.
#*Each series' base is remembered as an actual SHA1. That means that you need to manually adjust the 2nd series' base once it conflicts with earlier versions of the 1st series.
#*Solutions for both issues are planned, but not implemented yet.
 
==Pushing Multiple Series at Once==
 
gpush will currently reject multiple source specifications on the command line, and it wouldn't be entirely trivial to make it work. However, this is not expected to be a significant limitation, as it is possible to organize series with --group and --exclude, and "flush" these assignments with --all.


gpush will currently reject multiple source specifications on the command line, and it wouldn't be entirely trivial to make it work. Other than when chaining series (case 2. above), this is not expected to be a significant limitation given the existence of the --all mode in combination with the --group and --exclude modes.
==Restoring PatchSets==


=== Multiple Conflicts During gpick ===
If someone pushed a bad PatchSet over yours and you simply want to restore the previous one, you need to:
 
*Make an empty amend of your local commit as you would when not using gpush
*&lt;code&gt;git gpick --ignore&lt;/code&gt; the bad remote modification
*git gpush as usual
 
You may also ask an admin to delete the PatchSet if it qualifies as outright vandalism.
 
==Multiple Conflicts During gpick==


It may happen that an attempt to update a series will lead to multiple conflicts, some of which need to be --force'd while others --ignore'd. The solution is to update in several steps, each time passing the relevant switch and ranges that denote only parts of the series. Unlike in gpush, in gpick this will not change the grouping of the series (the series structure present on Gerrit will be applied).
It may happen that an attempt to update a series will lead to multiple conflicts, some of which need to be --force'd while others --ignore'd. The solution is to update in several steps, each time passing the relevant switch and ranges that denote only parts of the series. Unlike in gpush, in gpick this will not change the grouping of the series (the series structure present on Gerrit will be applied).


=== Merges ===
==Merges==


gpush works just fine with merges, but gpick does not support manipulating them. However, there is an easy workaround:
gpush works just fine with merges, but gpick does not support manipulating them. However, there is an easy workaround:
* check out the merge via the usual Gerrit download command
 
* sync the state with <tt>git gpick --check --all</tt>
*Check out the merge via the usual Gerrit download command
* now you can amend the merge and gpush it as if you had created it locally
*Sync the state with &lt;code&gt;git gpick --check --all&lt;/code&gt;
*Now you can amend the merge and gpush it as if you had created it locally

Latest revision as of 08:49, 27 June 2024

This page contains some background information for the git-gpush, git-gpick, git-gpull, and git-ggc scripts found in the qt/qtrepotools repository.

Note: there is no need to install anything - just clone the repository (qt5's init-repository does that for you) and add its bin/ sub-directory to your $PATH.

Your system must have perl installed, and you may need to install some additional modules (e.g., Term::ReadKey and JSON), which can easily be done with cpan, or preferably your distro's package manager. You do NOT need to install anything on Windows - Git for Windows comes with perl, and extending PATH per the above will make calls like git gpush (without a dash) just work.

Overview

The general idea of these scripts is to make working with Gerrit more transparent:

  • You can keep many unrelated Changes on one local branch without creating spurious dependencies between them on Gerrit. While you can achieve the same by having a separate local branch for every series, having all Changes on a single branch is much more convenient. This is sometimes called "stacked branches".
  • You can push updates to specific Changes without causing churn in unrelated Changes.
  • You can pull as often as you want, including right before pushing, as you would usually do in a git pull --rebase based workflow. Without git-gpush, this would unnecessarily rebase previously pushed Changes, which would make Gerrit's inter-diff feature slower and much noisier.

You may have noticed that these major features are geared towards making not your life easier, but that of your reviewers. Until roles are switched, that is.

The way how these scripts achieve this is by letting you manage "virtual branches" (called "series") within your local working branch. When a series is pushed, its commits are rebased (usually to a commit from the upstream branch) to isolate it from other local Changes. The working tree is not affected by this in any way, so no unnecessary rebuilds result from this.

More Features

git gpush

  • Updates to the Changes pushed from other clones of the target repository (or made directly on Gerrit) are detected, so you won't accidentally overwrite them.
  • The target branch of each series is remembered, server-side moves are tracked, and attempts to create duplicates by pushing for the wrong branch are rejected by default.
  • The scripts remember which Changes belong to a series, so once you pushed it, the complete series can be identified by specifying a single Change (or none at all, if it's HEAD). This saves you from needing to keep track of how many Changes are in the series. You don't even need to use the currently last Change in the series. Also, commits can be specified by Change-Id. Taken together, these features make it possible to use the same command line to repeatedly re-push the same series regardless of any local history rewriting.
  • Or you can just push out all local series in one go.
  • An overview of the pushed Changes and their status relative to Gerrit is displayed.
  • Pushes which would be rejected by Gerrit are detected upfront.
  • Unsquashed fixup commits and merges of non-upstream commits are rejected.
  • Changes duplicated between local branches are detected, and an authoritative source branch for actual pushes can be set.
  • Reviewers/CCs can be conveniently added by their IRC nickname.
  • gpush can also be used without Gerrit at all, to feed a patch/e-mail based review workflow.

git gpick

This script allows you to download pending Changes into your local branch, for local testing, reviewing and amending.

  • Notable differences from Gerrit's built-in copy&paste-able download command lines:
    • Unlike 'cherry-pick', it can download an entire series in one go, so you just need to copy the Change-Id of the last Change and execute a single short command.
    • Unlike 'checkout', your local Changes are not "hidden" on another branch, so you don't need to "switch contexts" (rebuild lots of stuff, deal with bugs you already fixed, etc.).
    • Unlike 'pull', no merge commit is introduced, so local history rewriting remains unproblematic.
  • The downloaded Changes are a separate series, so there is no danger that you accidentally re-push them together with other Changes.
  • Changes which already exist locally are replaced, so it is possible to pull in updates which have been pushed from other clones (or done directly on Gerrit).
    • This checks for conflicts, so you won't lose concurrent local modifications.
    • Merging of non-overlapping concurrent modifications is possible.
    • It's also possible to apply only certain parts of the updated Changes, e.g., the commit message.
    • There also exists the --check mode which only shows the updates on Gerrit without replacing any local commits.
      • This being a simple 3-way diff, it also displays local modifications with more granularity than git-gpush does.

Getting Started

As usual, you should start with some solid RTFM: ;)

$ git gpush -h
$ git gpick -h
$ git gpull -h
$ git ggc -h

(git help gpush, etc. do not work, because that would require installing man pages, which cannot be done for in-checkout execution).

Get all your pending changes onto the current branch, in case you kept them on separate branches:

$ git cherry-pick <…>

Alternatively, you could merge all your local branches and then rebase.

If you have pending Changes for the given branch on Gerrit, you need to synchronize (bootstrap) the state:

$ git gpick --check --all

To get an idea about what gpush thinks you have in your branch:

$ git gpush --list

Group remaining loose Changes into series:

$ git gpush --group :3
$ git gpush --group ~3:5
<…>

Group all Changes in the current branch into one series:

$ git gpush --group ..

Get a more thorough overview:

$ git gpush --list-online

Push out any modified Changes:

$ git gpush --all

Push out only the current commit:

$ git gpush :1

If Something Goes Wrong ...

  • If you get something that seems incomprehensible: slow down, think about it, don't use --force-* options right away
  • Do not fall back to manual pushing/picking, either - the scripts offer a solution for almost every situation
    • If you do after all (because you hit a bug or a documented limitation), always re-sync the state right afterwards
  • If you get a crash or other behavior that cannot be reasonably explained, make a bug report:
    • Make a JIRA task against QTQAINFRA/Git, and make sure it is assigned to me (Oswald Buddenhagen), or contact me directly via Matrix/IRC (ossi)
    • Add the recent shell history including output, especially the backtrace if you get one, obviously
      • If the problem is reproducible, re-run the failing command with --debug
    • Add a snapshot of the git/gpush state, created with:
      git bundle create gpush-state.bundle $(git for-each-ref --format='%(refname)' refs/gpush/ refs/heads/) --not $(git for-each-ref --format='%(refname)' refs/remotes/origin/)
  • If you manage to get the scripts so confused that none of the documented overrides help, you can throw away the complete state with:
    git update-ref -d refs/gpush/state
    note that it is very unlikely that you actually need to do that; and before you do, please save a snapshot and make a bug report
  • If you get errors: Can't locate Term/ReadKey.pm in @INC (you may need to install the Term::ReadKey module) Can't locate JSON.pm in @INC (you may need to install the JSON module) then in Debian/Ubuntu you should do sudo apt-get install -y libterm-readkey-perl libjson-perl

Examples

To create a series consisting of two Changes, and push it, adding two reviewers:

<hack hack hack>
$ git commit -a
<hack hack hack>
$ git commit -a
$ git gpush :2 +someguy +anotherguy@example.com

Amend and re-push the series:

$ git rebase -i @{u}
<…>
$ git gpush

Add another Change to the series:

<hack hack hack>
$ git commit -a
$ git gpush -e

Re-push the series when it's not at the top any more:

$ git gpush I123415affe

Download somebody else's series:

$ git gpick +Ie3f7567639cc05e03257cd5bac133714e5c4f068

No need to worry about accidentally re-pushing it any more!

Update your local copy after somebody has been messing with your Change (gpush will tell you about it, so you won't overwrite the Changes accidentally):

$ git gpick I9ce6d9e7a3

pull/rebase your local series:

$ git gpull

Answering Criticism

Shouldn't this be implemented by the server instead of client-side scripts?

Some of the problems could be addressed on the server side, but others are inherently client-side:

  • The automatic "downbasing" to keep a Change's base constant even after it has been rebased locally could be done on the server, indeed.
  • The management of series could - in principle - be done on the server. However:
    • This requires additional -o options which would need to be passed to git push. Nobody wants to do that by hand, so you'd end up with a script anyway.
    • This would upload unrelated Changes just to discard them immediately.
  • The status report can be done by the server - in fact, it already is done, though without giving per-Change information. However, for obvious reasons this happens only after uploading the Changes, which means you lose the potential for entirely avoiding doomed uploads.
  • The protection against overwriting concurrent updates is inherently client-side, as only the particular clone can know what was pushed from it the last time.
  • The reviewer add function as such is easy enough to use via -o options, but the IRC alias lookup isn't.
  • Identifying commits by Change-Id must be done client-side, as otherwise git doesn't even know what to push to the server.
  • git-gpick is rather obviously a client-side tool.

This changes my workflow! This is unacceptable!

If you refuse to take advantage of the automated series management gpush offers, the only necessary change in workflow would be consistently using git gpush .. (plus --rebase after conflicted pulls/rebases) instead of git push gerrit HEAD:refs/for/foo. If you are convinced that this is unacceptable, you should probably rethink your priorities.

It's a new tool! It's harder for newbies!

Pushing to Gerrit already requires installing the Change-Id-creating commit-msg hook. Just cloning another (small) repository to get access to the scripts doesn't seem terribly hard in comparison. Also, qt5's init-repository script will clone the repository by default anyway, so the only thing necessary is adding the bin directory to git's PATH.

And the basic usage of gpush is unarguably easier than that of plain git.

It makes Qt different from other Gerrit-using projects!

These scrips are in no way specific to Qt. Anyone can use them with any Gerrit installation. Upstreaming will be considered at some point.

I have my own scripts/aliases!

Good for you! But switching will be well worth it, even if you have to retrain your finger memory. Promise!

FWIW, I have aliased git-gpush to ggp, git-gpick to ggcp, and git-gpull to ggl.

Advanced topics

Series Spanning Multiple Branches

It is sometimes necessary to fix bugs before adding a new feature. The bugfixes should typically target a stable branch, while the feature is for dev.

gpush supports this scenario, but not optimally - Changes within the same series cannot have different attributes (like different target branches). While this would be supportable to a certain degree, the user interface gets really messy quickly.

Therefore, you have essentially two options, each with its own caveats:

  1. Target a single branch and move some Changes right before final approval and integration.
    • This confuses reviewers, so definitely requires additional communication on Gerrit.
    • It's better to target the stable branch first, because these Changes need to be integrated first. Otherwise you'd need to move them back and forth if they require amending.
      • You can do that just fine while working on dev locally, provided there are no merge conflicts.
    • gpush will adjust to the re-targeting and the already integrated Changes without requiring extra work or producing churn.
  2. You can designate two separate series in gpush, basing the feature series on the pending bugfix series by using the --onto switch.
    • You need to push the series one after another each time, unless you use --all mode.
    • Each series' base is remembered as an actual SHA1. That means that you need to manually adjust the 2nd series' base once it conflicts with earlier versions of the 1st series.
    • Solutions for both issues are planned, but not implemented yet.

Pushing Multiple Series at Once

gpush will currently reject multiple source specifications on the command line, and it wouldn't be entirely trivial to make it work. However, this is not expected to be a significant limitation, as it is possible to organize series with --group and --exclude, and "flush" these assignments with --all.

Restoring PatchSets

If someone pushed a bad PatchSet over yours and you simply want to restore the previous one, you need to:

  • Make an empty amend of your local commit as you would when not using gpush
  • <code>git gpick --ignore</code> the bad remote modification
  • git gpush as usual

You may also ask an admin to delete the PatchSet if it qualifies as outright vandalism.

Multiple Conflicts During gpick

It may happen that an attempt to update a series will lead to multiple conflicts, some of which need to be --force'd while others --ignore'd. The solution is to update in several steps, each time passing the relevant switch and ranges that denote only parts of the series. Unlike in gpush, in gpick this will not change the grouping of the series (the series structure present on Gerrit will be applied).

Merges

gpush works just fine with merges, but gpick does not support manipulating them. However, there is an easy workaround:

  • Check out the merge via the usual Gerrit download command
  • Sync the state with <code>git gpick --check --all</code>
  • Now you can amend the merge and gpush it as if you had created it locally