#!/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] [...] 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: ... 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= 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= In setup mode, use the specified remote as an information source. By default, the current branch's upstream is used. -u, --url= In setup mode, use the specified git URL instead of trying to derive it from the base remote. -p, --user= In setup mode, use the specified user (subdirectory) instead of trying to derive it from the username in the URL. -s, --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;