summaryrefslogtreecommitdiff
path: root/bin/git-push-all
blob: 8fec9ec721fb65694142fde180e02f0ccf49942d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#!/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 <https://www.gnu.org/licenses/>.

# 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;
    }
}