summaryrefslogtreecommitdiffstats
path: root/bin/git-ppush
diff options
context:
space:
mode:
authorOswald Buddenhagen <oswald.buddenhagen@digia.com>2013-02-08 18:50:54 +0100
committerOswald Buddenhagen <oswald.buddenhagen@digia.com>2013-02-15 15:53:33 +0100
commit6a672270cc3300606099b17d96ee94a0859bf567 (patch)
treea8d3fad49d36828650c7adc909685ca630955032 /bin/git-ppush
parent6fe754f2992e312f1eaab81d268db03266ea66b9 (diff)
add git-ppush: easy backup of local branches to personal namespace
Change-Id: Idb35f67f54287ab27884fae3bd567b395aab5528 Reviewed-by: Lars Knoll <lars.knoll@digia.com> Reviewed-by: Friedemann Kleint <Friedemann.Kleint@digia.com> Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@digia.com>
Diffstat (limited to 'bin/git-ppush')
-rwxr-xr-xbin/git-ppush297
1 files changed, 297 insertions, 0 deletions
diff --git a/bin/git-ppush b/bin/git-ppush
new file mode 100755
index 0000000..e7faa2a
--- /dev/null
+++ b/bin/git-ppush
@@ -0,0 +1,297 @@
+#!/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.
+#
+
+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) 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
+}
+
+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;
+ }
+ $url = `git config remote.$base.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;