From 6a672270cc3300606099b17d96ee94a0859bf567 Mon Sep 17 00:00:00 2001
From: Oswald Buddenhagen <oswald.buddenhagen@digia.com>
Date: Fri, 8 Feb 2013 18:50:54 +0100
Subject: [PATCH] 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>
---
 bin/git-ppush | 297 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 297 insertions(+)
 create mode 100755 bin/git-ppush

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;
-- 
GitLab