#!/usr/bin/env perl # git-push-all -- intelligently push most branches # Copyright (C) 2016, 2019, 2020, 2022 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 . # 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, force push it there # # 2. Otherwise, if remote.pushDefault is set and a branch of the same name # already exists on that remote, push it there # # 3. Otherwise, if it is tracking a remote branch that exists, push it there # # 4. Otherwise, emit a warning and skip over that branch. # # 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, to avoid errors. # Enhancement: --confirm to show what will be pushed to where, and ask # for confirmation: 'branch -> remote#target' use 5.028; use strict; use warnings; use lib "$ENV{HOME}/src/dotfiles/perl5"; use Cwd; use Git::Wrapper; use Local::Util::Git qw(unpushed_tags); my $git = Git::Wrapper->new(getcwd); my @local_branches = $git->for_each_ref({ format => '%(refname)' }, "refs/heads"); my %remote_branches = map +($_ => undef), $git->for_each_ref({ format => '%(refname)' }, "refs/remotes"); my ($pushDefault) = $git->config(qw|--local --get --default|, "", "remote.pushDefault"); my %pushes; my $dry_run = grep $_ eq "-n", @ARGV; foreach my $branch (@local_branches) { (my $short_branch = $branch) =~ s#^refs/heads/##; my ($pushRemote) = $git->config(qw|--local --get --default|, "", "branch.$short_branch.pushRemote"); my ($tracking) = $git->for_each_ref({ format => "%(upstream)" }, $branch); my ($tracking_remote) = $tracking =~ m#refs/remotes/([^/]+)/# if $tracking; my $need_pull_tracking = $git->for_each_ref({ contains => $short_branch }, $tracking) if $tracking; # note that except in the case of a defined pushRemote we don't # push unless the branch already exists on the remote: this script # avoids creating new branches if ($pushRemote) { say "I: would force push $short_branch to $pushRemote (its pushRemote)" if $dry_run; push $pushes{$pushRemote}->@*, "+$branch"; } elsif ($pushDefault and exists $remote_branches{"refs/remotes/$pushDefault/$short_branch"}) { say "I: would push $short_branch to $pushDefault (the remote.pushDefault)" if $dry_run; push $pushes{$pushDefault}->@*, $branch; } elsif (!$pushDefault and $tracking and !$need_pull_tracking and exists $remote_branches{$tracking}) { say "I: would push $short_branch to its tracking branch, $tracking" if $dry_run; push $pushes{$tracking_remote}->@*, $branch; } elsif (!$need_pull_tracking) { say "W: couldn't find anywhere to push $branch"; say "I: perhaps \`git branchmove\` it somewhere, " . "or set its pushRemote for -f pushes\n"; } } $dry_run and exit; foreach my $remote (keys %pushes) { # TODO if $remote eq $pushDefault, consider s/follow-// below (and # pushRemote of master branch, if that exists?) # I almost certainly want all tags on that remote (e.g. an alioth repo) my @branches = $pushes{$remote}->@*; my @args = qw(--follow-tags); push @args, "--no-verify" if grep $_ eq "--no-verify", @ARGV; # bypass Git::Wrapper which can hang pushing to salsa system "git", "push", @args, $remote, @branches; exit $? >> 8 if $?; } # Now find any tags that have not been pushed to any remote. # --follow-tags should avoid this, but sometimes tags fall through the # gaps. It will also catch unannotated tags, since --follow-tags # ignores those, and I probably don't want them # TODO if this turns out to be slow, split out into a script run # weekly as part of ~/bin/sysmaint # TODO definitely split out because should be run as a safety catch by # src-unregistered unless (grep $_ eq "--no-tags", @ARGV) { if (my @unpushed_tags = unpushed_tags) { say "E: the following tags have not been pushed to any remote:"; say join ", ", @unpushed_tags; exit 1; } }