summaryrefslogtreecommitdiffstats
path: root/bin/git-ppush
blob: ef0ecd5aa0130e43c9f47a34af3787884e5d049e (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
#!/usr/bin/perl
# Copyright (C) 2015 The Qt Company Ltd.
# Contact: http://www.qt.io/licensing/
#
# You may use this file under the terms of the 3-clause BSD license.
# See the file LICENSE from this package for details.
#

package Git::PPush;

use strict;
use warnings;

# 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 ppush [options] [<refspec>...]

    Pushes local branches to a personal namespace on a git server.

    A git repository has two standard namespaces: heads and tags, which
    are automatically added when setting up remotes.
    But it is possible to put refs into non-standard namespaces as well.
    These namespaces can be used to make backups or share work somewhat
    silently.

Options:
    <refspec>...
        The format is the same as for a regular git push.
        However, the destination ref is automatically prefixed with the
        personal namespace.

    -a, --all
        Push all local branches instead of the current one.

    -r, --remote=<remote>
        Use specified remote instead of 'personal'.

    --prune
        Remove remote branches that do not have a local counterpart.

    --delete
        All listed refs are deleted from the remote repository.
        This is the same as prefixing all refs with a colon.

    -f, --force
        Force push even if remote commits will be lost.

    -n, --dry-run
        Do everything except actually send the updates.

    -v, --verbose
        Shows the final 'git push' command as a comma-separated list of
        arguments.

    --setup
        Instead of pushing, create the personal remote.

    -b, --base=<remote>
        In setup mode, use the specified remote as an information source.
        By default, the current branch's upstream is used.

    -u, --url=<url>
        In setup mode, use the specified git URL instead of trying to
        derive it from the base remote.

    -p, --user=<user>
        In setup mode, use the specified user (subdirectory) instead of
        trying to derive it from the username in the URL.

    -s, --namespace=<namespace>
        In setup mode, use the specified namespace instead of 'personal'.

Examples:

    Backup local branches:

        $ git ppush --setup --base=gerrit    # Needed only once
        $ git ppush -f                       # Backup current branch
        $ git ppush --force --all --prune    # Synchronize everything

    Get a colleague's work:

        $ git ppush --setup --base=gerrit --remote=ossis --user=buddenha
        $ git fetch ossis
        $ git log -p ossis/master

Copyright:
    Copyright (C) 2015 The Qt Company Ltd.
    Contact: http://www.qt.io/licensing/

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

sub parse_arguments
{
    my ($self, @arguments) = @_;

    while (scalar @arguments) {
        my $arg = shift @arguments;

        if ($arg eq "-?" || $arg eq "--?" || $arg eq "-h" || $arg eq "--help") {
            $self->usage();
            exit 0;
        } elsif ($arg eq "-v" || $arg eq "--verbose") {
            $self->{'verbose'} = 1;
            push @{$self->{'arguments'}}, $arg;
        } elsif ($arg eq "-n" || $arg eq "--dry-run") {
            $self->{'dry-run'} = 1;
            push @{$self->{'arguments'}}, $arg;
        } elsif ($arg eq "-r") {
            $self->{'remote'} = shift @arguments;
        } elsif ($arg =~ /^--remote=(.*)/) {
            $self->{'remote'} = $1;
        } elsif ($arg eq "-a" || $arg eq "--all") {
            $self->{'refs'} = [ "refs/heads/*" ];
        } elsif ($arg eq "--prune") {
            $self->{'prune'} = 1;
            push @{$self->{'arguments'}}, $arg;
        } elsif ($arg eq "--delete") {
            $self->{'delete'} = 1;
        } elsif ($arg eq "-f" || $arg eq "--force") {
            $self->{'force'} = 1;
        } elsif ($arg eq "--setup") {
            $self->{'setup'} = 1;
        } elsif ($arg eq "-b") {
            $self->{'base'} = shift @arguments;
        } elsif ($arg =~ /^--base=(.*)/) {
            $self->{'base'} = $1;
        } elsif ($arg eq "-u") {
            $self->{'url'} = shift @arguments;
        } elsif ($arg =~ /^--url=(.*)/) {
            $self->{'url'} = $1;
        } elsif ($arg eq "-s") {
            $self->{'namespace'} = shift @arguments;
        } elsif ($arg =~ /^--namespace=(.*)/) {
            $self->{'namespace'} = $1;
        } elsif ($arg eq "-p") {
            $self->{'user'} = shift @arguments;
        } elsif ($arg =~ /^--user=(.*)/) {
            $self->{'user'} = $1;
        } elsif ($arg =~ /^-/) {
            die "Unrecognized option ".$arg."\n";
        } else {
            push @{$self->{'refs'}}, $arg;
        }
    }

    if ($self->{'setup'}) {
        die "Naming refspecs is incompatible with --setup.\n" if (@{$self->{'refs'}});
        die "--prune is incompatible with --setup\n" if ($self->{'prune'});
        die "--delete is incompatible with --setup\n" if ($self->{'delete'});
        die "--force is incompatible with --setup\n" if ($self->{'force'});
    } else {
        die "--base is valid only in --setup mode.\n" if ($self->{'base'});
        die "--url is valid only in --setup mode.\n" if ($self->{'url'});
        die "--namespace is valid only in --setup mode.\n" if ($self->{'namespace'});
        die "--user is valid only in --setup mode.\n" if ($self->{'user'});
    }
}

sub spawn_cmd
{
    my ($self, @cmd) = @_;

    print '+'.join(',', @cmd)."\n" if ($self->{'verbose'});
    system(@cmd) and exit $? if (!$self->{'dry-run'});
}

sub run_setup
{
    my ($self) = @_;

    my $url = $self->{'url'};
    my $user = $self->{'user'};
    if (!$url) {
        my $base = $self->{'base'};
        if (!$base) {
            my $ref = `git symbolic-ref -q HEAD`;
            die "Not on a branch. Cannot determine base remote.\n" if (!$ref);
            chomp $ref;
            $ref =~ s,^refs/heads/,,;
            $base = `git config branch.$ref.remote`;
            die "Have no upstream. Cannot determine base remote.\n" if (!$base);
            chomp $base;
        }
        # Personal repos can only be in Gerrit, but the base repo might be using
        # a mirror. Try the pushurl first, if that has been set up, it most likely
        # will point to Gerrit. It will be set as .url, so both push and fetch work.
        $url = `git config remote.$base.pushurl`;
        $url = `git config remote.$base.url` if (!$url);
        die "Cannot determine URL of base remote. Try --url.\n" if (!$url);
        chomp $url;
    }
    if (!$user) {
        $url =~ m,(?:[^:]+://)?([^\@]+)\@.*, or
            # FIXME: could try to query ssh config here.
            die "Cannot determine user from URL. Try --user.\n";
        $user = $1;
    }
    my $remote = $self->{'remote'};
    my $namespace = $self->{'namespace'};
    $namespace = "personal" if (!$namespace);
    $self->spawn_cmd("git", "config", "remote.$remote.url", $url);
    $self->spawn_cmd("git", "config", "remote.$remote.fetch",
                                      "+refs/$namespace/$user/*:refs/remotes/$remote/*");
}

sub push_commits
{
    my ($self) = @_;

    my $force = $self->{'force'};
    my $remote = $self->{'remote'};
    my $refspec = `git config remote.$remote.fetch`;
    die "Invalid remote specified.\n" if (!$refspec);
    chomp $refspec;
    $refspec =~ s,^\+?([^*]+).*,$1,;
    my @refs = @{$self->{'refs'}};
    if (!@refs) {
        my $ref = `git symbolic-ref -q HEAD`;
        die "No refspecs given and not on a branch.\n" if (!$ref);
        chomp $ref;
        $ref =~ s,^refs/heads/,,;
        push @refs, $ref;
    }

    my @gitcmd = ("git", "push");
    push @gitcmd, @{$self->{'arguments'}};
    push @gitcmd, $remote;
    foreach my $ref (@refs) {
        my ($pfx, $src, $dst) = ("", "", "");
        $ref =~ s,^(\+?)(.*),$2,;
        $pfx = $1;
        $pfx = '+' if ($force);
        if ($ref =~ /(.*):(.*)/) {
            $src = $1;
            $dst = $2;
        } else {
            $src = $ref if (!$self->{'delete'});
            $dst = $ref;
        }
        $dst =~ s,^refs/heads/,,;
        push @gitcmd, $pfx.$src.':'.$refspec.$dst;
    }

    $self->{'dry-run'} = 0;  # we already have it in git's command line
    $self->spawn_cmd(@gitcmd);
    exit 0;
}

sub new
{
    my ($class, @arguments) = @_;

    my $self = {};
    bless $self, $class;

    $self->{'verbose'} = 0;
    $self->{'dry-run'} = 0;

    $self->{'setup'} = 0;
    $self->{'remote'} = "personal";

    # push mode
    $self->{'refs'} = [];
    $self->{'prune'} = 0;
    $self->{'delete'} = 0;
    $self->{'force'} = 0;

    # setup mode
    $self->{'base'} = "";
    $self->{'url'} = "";
    $self->{'namespace'} = "";
    $self->{'user'} = "";

    $self->{'arguments'} = [];

    $self->parse_arguments(@arguments);
    return $self;
}

sub run
{
    my ($self) = @_;
    if ($self->{'setup'}) {
        $self->run_setup;
    } else {
        $self->push_commits;
    }
}

#==============================================================================

Git::PPush->new(@ARGV)->run if (!caller);
1;