I maintain Debian packages for several projects which are hosted on
GitHub. I have a master packaging branch containing both upstream’s
code, and my debian/
subdirectory containing the packaging control
files. When upstream makes a new release, I simply merge their
release tag into master
: git merge 1.2.3
(after reviewing the
diff!).
Packaging things for Debian turns out to be a great way to find small bugs that need to be fixed, and I end up forwarding a lot of patches upstream. Since the projects are on GitHub, that means forking the repo and submitting pull requests. So I end up with three remotes:
origin
- the Debian git server
upstream
- upstream’s GitHub repo from which I’m getting the release tags
fork
- my GitHub fork of upstream’s repo, where I’m pushing bugfix branches
I can easily push individual branches to particular remotes. For
example, I might say git push -u fork fix-gcc-6
. However, it is
also useful to have a command that pushes everything to the places it
should be: pushes bugfix branches to fork
, my master packaging
branch to origin
, and definitely doesn’t try to push anything to
upstream
(recently an upstream project gave me push access because I
was sending so many patches, and then got a bit annoyed when I pushed
a series of Debian release tags to their GitHub repo by mistake).
I spent quite a lot of time reading git-config(1)
and git-push(1)
,
and came to the conclusion that there is no combination of git
settings and a push command that do the right thing in all cases.
Candidates, and why they’re insufficient:
git push --all
- I thought about using this with the
remote.pushDefault
andbranch.*.pushRemote
configuration options. The problem is thatgit push --all
pushes to only one remote, and it selects it by looking at the current branch. If I ran this command for all remotes, it would push everything everywhere. git push <remote> :
for each remote- This is the “matching push strategy”. It will push all branches that already exist on the remote with the same name. So I thought about running this for each remote. The problem is that I typically have different master branchs on different remotes. The
fork
andupstream
remotes have upstream’s master branch, and theorigin
remote has my packaging branch.
I wrote a perl script implementing git push-all
, which does the
right thing. As you will see from the description at the top of the
script, it uses remote.pushDefault
and branch.*.pushRemote
to
determine where it should push, falling back to pushing to the remote
the branch is tracking. If won’t push something when all three of
these are unspecified, and more generally, it won’t create new remote
branches except in the case where the branch-specific setting
branch.*.pushRemote
has been specified. Magit
makes it easy to set remote.pushDefault
and branch.*.pushRemote
.
I have this in my ~/.mrconfig:
git_push = git push-all
so that I can just run mr push
to ensure that all of my work has
been sent where it needs to be (see
myrepos).
#!/usr/bin/perl
# git-push-all -- intelligently push most branches
# Copyright (C) 2016 Sean Whitton
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Prerequisites:
# The Git::Wrapper, Config::GitLike, and List::MoreUtils perl
# libraries. On a Debian system,
# apt-get install libgit-wrapper-perl libconfig-gitlike-perl \
# liblist-moreutils-perl
# Description:
# This script will try to push all your branches to the places they
# should be pushed, with --follow-tags. Specifically, for each branch,
#
# 1. If branch.pushRemote is set, push it there
#
# 2. Otherwise, if remote.pushDefault is set, push it there
#
# 3. Otherwise, if it is tracking a remote branch, push it there
#
# 4. Otherwise, exit non-zero.
#
# If a branch is tracking a remote that you cannot push to, be sure to
# set at least one of branch.pushRemote and remote.pushDefault.
use strict;
use warnings;
no warnings "experimental::smartmatch";
use Git::Wrapper;
use Config::GitLike;
use List::MoreUtils qw{ uniq apply };
my $git = Git::Wrapper->new(".");
my $config = Config::GitLike->new( confname => 'config' );
$config->load_file('.git/config');
my @branches = apply { s/[ \*]//g } $git->branch;
my @allBranches = apply { s/[ \*]//g } $git->branch({ all => 1 });
my $pushDefault = $config->get( key => "remote.pushDefault" );
my %pushes;
foreach my $branch ( @branches ) {
my $pushRemote = $config->get( key => "branch.$branch.pushRemote" );
my $tracking = $config->get( key => "branch.$branch.remote" );
if ( defined $pushRemote ) {
print "I: pushing $branch to $pushRemote (its pushRemote)\n";
push @{ $pushes{$pushRemote} }, $branch;
# don't push unless it already exists on the remote: this script
# avoids creating branches
} elsif ( defined $pushDefault
&& "remotes/$pushDefault/$branch" ~~ @allBranches ) {
print "I: pushing $branch to $pushDefault (the remote.pushDefault)\n";
push @{ $pushes{$pushDefault} }, $branch;
} elsif ( !defined $pushDefault && defined $tracking ) {
print "I: pushing $branch to $tracking (probably to its tracking branch)\n";
push @{ $pushes{$tracking} }, $branch;
} else {
die "E: couldn't find anywhere to push $branch";
}
}
foreach my $remote ( keys %pushes ) {
my @branches = @{ $pushes{$remote} };
system "git push --follow-tags $remote @branches";
exit 1 if ( $? != 0 );
}