summaryrefslogtreecommitdiffstats
path: root/bin/git-gpush
blob: 5b840a3bf5f9705654773fcf6dd460da629bbe1c (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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#!/usr/bin/env perl
# Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
# Contact: http://www.qt-project.org/legal
#
# You may use this file under the terms of the 3-clause BSD license.
# See the file LICENSE from this package for details.
#

use strict;
use warnings;

use Carp;
$SIG{__WARN__} = \&Carp::cluck;

use File::Basename;

# Cannot use Pod::Usage for this file, since git on Windows will invoke its own perl version, which
# may not (msysgit for example) support this module, even if it's considered a Core module.
sub usage
{
    print << "EOM";
Usage:
    git gpush [opts] [remote] [[sha1/ref-from]:[ref-to]] [+<reviewer>] [=<CC user>] [-- <push opts>]

    Pushes changes to Gerrit and adds reviewers and CC to the patch
    sets

Description:
    This script is used to push patch sets to Gerrit, and at the same
    time add reviewers and CCs to the patch sets pushed.

    You can use email addresses, Gerrit usernames or aliases for the
    name of the reviewers/CCs.

    If no sha1 or ref-from is specified, 'HEAD' is used.

    If no ref-to is specified, the remote tracking branch for 'ref-from'
    is used as
        'refs/for/<remote tracking branch>'.

    If no remote is specified or configured, 'gerrit' is used. You may
    configure a remote like this:
        git config gpush.remote <remote name>

    If all the options above have been populated, the remainder
    options are passed on directly to the normal 'git push' command.
    If you want to avoid specifying all options first, any options
    specified after a '--' are also passed on directly to the
    underlying 'git push' command.

Options:
    -d, --draft
        Mark the pushed changes as drafts. This switch is usually
        unnecessary, as gpush will recognize WIP changes by subject.

    -p, --publish
        Do not mark the pushed changes as drafts even if they have
        WIP markers.

    --aliases
        Report all registered aliases and quit.

    -n, --dry-run
        Do everything except actually pushing any commits.

    -v, --verbose
        Show the resolved aliases, SHA1s of commits, and other information.

    -q, --quiet
        Suppress the usual output about what is pushed where.

    --debug
        Print debug information.

Configuring Aliases:
    Aliases are read from the
        .git-gpush-aliases
    located next to the script, then from the git config which may
    have aliases set either locally in the current repository,
    globally (in your ~/.gitconfig), or system-wide.

    You can add aliases to your global git config like this:
        git config --global gpush.alias.<alias key> <alias value>
    and if you only want it to be local to the current repository,
    just drop the --global option.
    Note that git config keys are constrained regarding allowed
    characters, so it is impossible to map some IRC nicks via git
    configuration.

    An alias may contain multiple comma-separated email addresses;
    for example, to set a single alias for an entire team.

    Inside .git-gpush-aliases, each alias may also be a comma-separated
    list, in case a user uses multiple handles.

Copyright:
    Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
    Contact: http://www.qt-project.org/legal

License:
    You may use this file under the terms of the 3-clause BSD license.
EOM
}

my $debug = 0;
my $verbose = 0;
my $quiet = 0;
my $dry_run = 0;

my $remote = "gerrit";
my $remote_override = 0;
my $ref_from = "HEAD";
my $ref_to = "";
my $ref_override = 0;
my $draft = 0;

my %aliases;

my @reviewers;
my @CCs;

my @arguments;

sub format_cmd(@)
{
    return join(' ', map { /\s/ ? '"' . $_ . '"' : $_ } @_);
}

sub read_git_line(@)
{
    print "+ " . format_cmd('git', @_) . "\n" if ($debug);
    open PROC, '-|', 'git', @_
        or die("Failed to run \"git\": $!\n");
    my $line = <PROC>;
    if (defined($line)) {
        chomp $line ;
        print "- $line\n" if ($debug);
    }
    close PROC;
    return $line;
}

sub parse_arguments(@)
{
    while (scalar @_) {
        my $arg = shift @_;

        if ($arg eq "-v" || $arg eq "--verbose") {
            $verbose = 1;
        } elsif ($arg eq "-q" || $arg eq "--quiet") {
            $quiet = 1;
        } elsif ($arg eq "--debug") {
            $debug = 1;
            $verbose = 1;
        } elsif ($arg eq "-n" || $arg eq "--dry-run") {
            $dry_run = 1;
        } elsif ($arg eq "-d" || $arg eq "--draft") {
            $draft = 1;
        } elsif ($arg eq "-p" || $arg eq "--publish") {
            $draft = -1;
        } elsif ($arg eq "--aliases") {
            foreach my $key (sort(keys %aliases)) {
                print "$key = $aliases{$key}\n";
            }
            exit 0;
        } elsif ($arg eq "-?" || $arg eq "--?" || $arg eq "-h" || $arg eq "--help") {
            usage();
            exit 0;
        } elsif ($arg eq "--") {
            push @arguments, @_;
            return;
        } elsif ($arg =~ /^\+(.+)/) {
            push @reviewers, split(/,/, lookup_alias($1));
        } elsif ($arg =~ /^\=(.+)/) {
            push @CCs, split(/,/, lookup_alias($1));
        } elsif ($arg =~ /^\-(.+)/) {
            push @arguments, $arg;
        } elsif (!$remote_override || !$ref_override) {
            if ($arg =~ /(.*):(.*)/) {
                $ref_from = $1 if (defined $1 && $1 ne "");
                $ref_to = $2 if (defined $2 && $2 ne "");
                $ref_override = 1;
            } else {
                $remote = $arg;
                $remote_override = 1;
            }
        } else {
            push @arguments, $arg;
        }
    }

    die("--quiet and --verbose/--debug are mutually exclusive.\n")
        if ($quiet && $verbose);

    if ($ref_to =~ s,^refs/for/,,) {
        die "Pushing to refs/for/ is inconsistent with the --draft option.\n" if ($draft > 0);
        print STDERR "Notice: it is unnecessary to specify refs/for/ in the target ref.\n"
            if (!$quiet);
    } elsif ($ref_to =~ s,^refs/drafts/,,) {
        die "Pushing to refs/drafts/ is inconsistent with the --publish option.\n" if ($draft < 0);
        if ($draft) {
            print STDERR "Notice: it is unnecessary to specify refs/drafts/ in the target ref.\n"
                if (!$quiet);
        } else {
            print STDERR "Notice: prefer the --draft option over specifying refs/drafts/ in the target ref.\n"
                if (!$quiet);
            $draft = 1;
        }
    }
}

sub fileContents($)
{
    my ($filename) = @_;

    my @contents = "";
    my $fh;
    if (-e $filename && open($fh, "< $filename")) {
        @contents = <$fh>;
        close $fh;
    }
    return @contents;
}

sub load_aliases()
{
    my $script_path = dirname($0);

    # Read aliases from .git-gpush-aliases file
    foreach my $line (fileContents("$script_path/.git-gpush-aliases")) {
        chomp $line;
        $line =~ s,(#|//).*$,,;             # Remove any comments
        if ($line =~ /([^ ]+)\s*=\s*(\S+)/) {  # Capture the alias
            for my $alias (split(/,/, $1)) {
                $aliases{$alias} = $2;
            }
        }
    }

    # Read aliases and configurations from git config
    my @gitconfigs = `git config --get-regexp gpush.*`;
    return if ($?); # just return if no git configs for gpush

    foreach (@gitconfigs) {
        if (/^gpush\.remote (\w+)/) {
            $remote = $2;
        } elsif (/^gpush\.ref-from (.+)/) {
            die("Configuring ref-from is not supported any more.\n");
        } elsif (/^gpush\.ref-to (.+)/) {
            die("Configuring ref-to is not supported any more.\n");
        } elsif (/^gpush\.alias\.([^ ]*) (.+)/) {
            $aliases{$1} = $2;
        } # else ignore
    }
}

sub lookup_alias($)
{
    my ($user) = @_;

    my $alias = $aliases{$user};
    if (defined $alias && $alias ne "") {
        print "Resolved $user to $alias.\n" if ($verbose);
        return $alias;
    }

    return $user;
}

sub add_reviewers()
{
    if (@reviewers || @CCs) {
        my @dudes;
        push @dudes, "--receive-pack=git receive-pack";
        push @dudes, map { " --reviewer=$_" } @reviewers;
        push @dudes, map { " --cc=$_" } @CCs;
        push @arguments, join('', @dudes); # Single argument to git push
    }
}

sub goto_gitdir()
{
    my $cdup = read_git_line('rev-parse', '--show-cdup');
    exit $? >> 8 if $?;
    chdir($cdup) unless $cdup eq "";
}

sub determine_target()
{
    # Detect tracking branch if ref-to is not set
    if ($ref_to eq "") {
        my $ref = $ref_from;
        $ref =~ s/[~^].*$//;
        my $sref = read_git_line("symbolic-ref", "-q", $ref);
        $ref = $sref if ($? == 0);
        $ref =~ s,^refs/heads/,,;
        read_git_line("rev-parse", "--verify", "-q", "refs/heads/".$ref);
        die "Cannot detect tracking branch, $ref is not a valid ref.\n" if ($? != 0);
        $ref_to = read_git_line("config", "branch.$ref.merge");
        die "Cannot detect tracking branch, 'git config branch.$ref.merge' failed.\n" if ($? != 0);
        $ref_to =~ s,^refs/heads/,,;
    }
}

sub push_patches()
{
    if (!$draft) {
        $_ = read_git_line('log', '--pretty=%s', '-1', $ref_from);
        exit $? >> 8 if ($? != 0);
        $draft = 1 if (/\bWIP\b|\*{3}|^(.)\1*$/i);
    }

    print "Pushing $ref_from for $ref_to on $remote ...\n" if (!$quiet);

    my @gitcmd = ("git", "push");
    push @gitcmd, '-v' if ($verbose);
    push @gitcmd, '-q' if ($quiet);
    push @gitcmd, '-n' if ($dry_run);
    push @gitcmd, @arguments;
    push @gitcmd, $remote, "$ref_from:refs/".($draft > 0 ? 'drafts' : 'for')."/$ref_to";

    print '+ '.format_cmd(@gitcmd)."\n" if ($debug);
    my $ex = system(@gitcmd);
    die("Failed to run \"git\": $!\n") if ($ex < 0);
    exit($ex >> 8) if ($ex);
}

load_aliases();
parse_arguments(@ARGV);
add_reviewers();
goto_gitdir();
determine_target();
push_patches();