Pārlūkot izejas kodu

Configuration model done. Proved out, now all we need is the hard part TM

George Baugh 1 gadu atpakaļ
vecāks
revīzija
ada0e825ec
1 mainītis faili ar 213 papildinājumiem un 45 dzēšanām
  1. 213 45
      bin/git-clone-entity

+ 213 - 45
bin/git-clone-entity

@@ -7,10 +7,12 @@ use warnings;
 
 use FindBin::libs;
 
+use Config::Simple;
 use Getopt::Long qw{GetOptionsFromArray};
 use Pod::Usage;
 use Pithub;
 use Gogs;
+use Git;
 
 use Term::ReadKey();
 use IO::Interactive::Tiny();
@@ -30,11 +32,116 @@ It will warn you whenever a repository is missing from either, so you can make i
 
 Using this you can easily migrate an organization from being entirely on github to using private resources or vice versa.
 
-=head1
+=head1 IMPORTANT
+
+This assumes that the repo names between the base and mirrors is identical.
+
+=head1 CONFIG FILE
+
+You will notice below that the options of this tool can be quite involved.
+
+To simplify deploying this tool across your organization, you can place a configuration file (Config::Simple) in ~/.git/clone-entity.cfg.  Example:
+
+	baseurl=https://my-gogs-install.test/api/v1
+	gogs=true
+	nossh=true
+	mirrors=https://api.github.com,https://premise-install.github.local/api
+	me=jane
+
+Ideally all your users have to do is specify which users/orgs to clone w/mirroring and you should be off to the races.
+
+The name of the setting will be pluralized for any option which may be passed multiple times below.
 
 =head1 USAGE
 
-git clone-entity --user $user1 --user $user2 --org $org1 --org $org2 --alias $user1:$mirror_domain:$mirrorUser1 --baseurl=https://my.local.install/ [--gogs] [--mirror https://github.com] [--help]
+git clone-entity --user $user1 --user $user2 --org $org1 --org $org2 --alias $user1:$mirror_domain:$mirrorUser1 --baseurl=https://my.local.install/ [--gogs] [--mirror https://github.com] [--nossh] [--insecure] [--help]
+
+=head1 OPTIONS
+
+=head3 me
+
+Your username on the baseurl.  Relevant to token use, what is visible, etc.
+
+	--me tarzan
+
+=head3 baseurl
+
+URI for your Git management solution.  Currently github and gogs are supported.
+
+	--baseurl https://api.github.com
+	--baseurl https://gogs.mydomain.test/api/v1
+
+=head3 gogs
+
+Whether your baseurl is a gogs installation.
+
+While the APIs between gogs and github are compatible there are slight differences which must be accounted for.
+
+	--gogs
+
+=head3 mirror
+
+URI for a git management solution you wish to use for mirroring the repos at the baseurl.  May be passed multiple times.
+
+	--mirror https://on-prem.github.local/api/
+
+=head3 token
+
+Token for a particular baseurl or mirror.  Of the format domain:token.
+
+	--token my.domain.test:DEADBEEF
+
+You can omit the auth token on gogs, as we can create them automatically (we will prompt for your password).
+
+=head3 user
+
+Clone all of this user's repositories.  May be passed multiple times.
+
+	--user fred
+
+=head3 org
+
+Clone all of this organization's repositories.  May be passed multiple times.
+
+	--org 'Granite-Industries'
+
+=head3 alias
+
+Map a user/org on your baseurl to a mirror.  Of the format base_user:mirror_domain:mirror_user.
+
+Obviously won't work if the mirror is on the same hostname as the baseurl; use a subdomain at the very least.
+
+	--alias george:sprockets.spacely.local:gjetson
+
+=head3 nossh
+
+Don't use SSH clone URIs.  Useful for read-only clones & deployments with no ssh-agent.
+
+	--nossh
+
+=head1 CONSEQUENTIAL OPTIONS
+
+=head3 insecure
+
+Allow insecure mirrors or baseurls.  This is just to prevent footgunning by passing auth over plaintext.
+
+	--insecure
+
+=head3 create
+
+Automatically create a copy of the repo on the mirror if it doesn't exist.
+
+	--create
+
+=head3 private
+
+If --create is passed, also mirror repositories marked as private, preserving privacy.
+
+=head3 sync
+
+Force push all refs onto the mirror(s).
+
+	--sync
 
 =cut
 
@@ -51,40 +158,75 @@ my $domainRipper = qr{^\w+://([\w|\.]+)};
 sub main {
     my @args = @_;
 
-    my $help;
-    my ($users, $orgs, $aliases, $tokens, $mirrors, $baseurl, $gogs, $me, $insecure) = ([],[],[],[],[],"", 0, "", 0);
+	my %options = (
+		help 	 => undef,
+		users	 => [],
+		orgs  	 => [],
+		aliases  => [],
+		tokens   => [],
+		mirrors  => [],
+		baseurl  => "",
+		me       => undef,
+		create   => undef,
+		sync     => undef,
+		gogs     => undef,
+		insecure => undef,
+		nossh    => undef,
+	);
+
+	# Allow options to override configuration
+	my $home = $ENV{HOME};
+	mkdir "$home/.git" unless -d "$home/.git";
+	my $config_file = "$home/.git/clone-entity.cfg";
+	if (-f $config_file) {
+		my $conf = Config::Simple->new($config_file);
+		my %config;
+		%config = %{$conf->param(-block => 'default')} if $conf;
+
+		# Merge the configuration with the options
+		foreach my $opt (keys(%options)) {
+			if ( ref $options{$opt} eq 'ARRAY' ) {
+				next unless exists $config{$opt};
+				my @arrayed = ref $config{$opt} eq 'ARRAY' ? @{$config{$opt}} : ($config{$opt});
+				push(@{$options{$opt}}, @arrayed);
+				next;
+			}
+			$options{$opt} = $config{$opt} if exists $config{$opt};
+		}
+	}
 
     GetOptionsFromArray(\@args,
-        'me=s'      => \$me,
-        'user=s@'   => \$users,
-        'alias=s@'  => \$aliases,
-        'token=s@'  => \$tokens,
-        'org=s@'    => \$orgs,
-        'baseurl=s' => \$baseurl,
-        'mirror=s@' => \$mirrors,
-        'gogs'      => \$gogs,
-        'insecure'  => \$insecure,
-        'help'      => \$help,
+        'me=s'      => \$options{me},
+        'user=s@'   => \$options{users},
+        'alias=s@'  => \$options{aliases},
+        'token=s@'  => \$options{tokens},
+        'org=s@'    => \$options{orgs},
+        'baseurl=s' => \$options{baseurl},
+        'mirror=s@' => \$options{mirrors},
+        'gogs'      => \$options{gogs},
+        'insecure'  => \$options{insecure},
+		'nossh'     => \$options{nossh},
+        'help'      => \$options{help},
     );
 
-    return _help() if $help;
-    return _help(1, "Must pass at least one user or organization") unless (@$users + @$orgs);
-    return _help(2, "Must pass baseurl") unless $baseurl;
-    return _help(3, "Must pass your username as --me") unless $me;
+    return _help() if $options{help};
+    return _help(1, "Must pass at least one user or organization") unless (@{$options{users}} + @{$options{orgs}});
+    return _help(2, "Must pass baseurl") unless $options{baseurl};
+    return _help(3, "Must pass your username as --me") unless $options{me};
 
     # Parse Alias mappings
     my %alias_map;
-    if (@$aliases) {
-        foreach my $arg (@$aliases) {
+    if (@{$options{aliases}}) {
+        foreach my $arg (@{$options{aliases}}) {
             my ($actual, $domain, $alias) = split(/:/, $arg);
             return _help(3, "aliases must be of the form user:domain:alias") unless $actual && $domain && $alias;
             $alias_map{$domain}{$actual} = $alias;
         }
     }
 
-    my ($primary_domain) = $baseurl =~ $domainRipper;
+    my ($primary_domain) = $options{baseurl} =~ $domainRipper;
     my %tokens;
-    foreach my $tok (@$tokens) {
+    foreach my $tok (@{$options{tokens}}) {
         my ($domain, $token) = split(/:/, $tok);
         return _help(4, "tokens must be of the form domain:token") unless $domain && $token;
         $tokens{$domain} = $token;
@@ -92,39 +234,38 @@ sub main {
 
     my $primary_token = $tokens{$primary_domain};
     my %args = (
-        user    => $me,
-        api_uri => $baseurl,
+        user    => $options{me},
+        api_uri => $options{baseurl},
     );
     $args{token} = $primary_token if $primary_token;
 
     # It's important which is the primary, because we can have only one pull url, and many push urls.
-    my $local  = $gogs ? Gogs->new(%args) : Pithub->new( %args );
+    my $local  = $options{gogs} ? Gogs->new(%args) : Pithub->new( %args );
 
     # If the primary is gogs and we have no token passed, let's make one.
 	my $password;
-	if (!$primary_token && $gogs) {
+	if (!$primary_token && $options{gogs}) {
 		_help(5, "Program must be run interactively to auto-create keys on Gogs installs.") unless IO::Interactive::Tiny::is_interactive();
 		# Stash the password in case we gotta clean up
 		$password = _prompt("Please type in the password for ".$local->user.":");
 		$primary_token = $local->get_token(
 			name     => "git-clone-entity",
 			password => $password,
-            insecure => $insecure,
+            insecure => $options{insecure},
 		);
         _help(6, "Could not fetch token from gogs!  Check that you supplied the correct username & password.") unless $primary_token;
         $local->token($primary_token);
 	}
 
-	my $cleanup = sub { _cleanup_token( $local, $password, $insecure ) if $password };
+	my $cleanup = sub { _cleanup_token( $local, $password, $options{insecure} ) if $password };
 
-	# TODO XXX this is not appending /api/v1 for some reason
-    my @repos_local = _fetch_all($local, $users, $orgs);
-    _help(7, "Server at $baseurl could not list repos!", $cleanup ) unless @repos_local;
+    my @repos_local = _fetch_all($local, $options{users}, $options{orgs});
+    _help(7, "Server at $options{baseurl} could not list repos!", $cleanup ) unless @repos_local;
 
-    my %repos_mirror;
-    foreach my $mirror_url (@$mirrors) {
+    my @repos_mirror;
+    foreach my $mirror_url (@{$options{mirrors}}) {
         my ($mirror_domain) = $mirror_url =~ $domainRipper;
-        my $muser = $me;
+        my $muser = $options{me};
         $muser = $alias_map{$mirror_domain}{$muser} if exists $alias_map{$mirror_domain}{$muser};
         my %margs = (
             user    => $muser,
@@ -133,26 +274,53 @@ sub main {
         $args{token} = $tokens{$mirror_domain} if $tokens{$mirror_domain};
 
         my $mirror = Pithub->new( api_uri => $mirror_url );
-        $repos_mirror{$mirror_url} = _fetch_all($mirror, $users, $orgs, \%alias_map);
-        _help(8, "The provided mirror ($mirror_url) could not list repos!", $cleanup ) unless @{$repos_mirror{$mirror_url}};
+		my @fetched = _fetch_all($mirror, $options{users}, $options{orgs}, \%alias_map);
+        _help(8, "The provided mirror ($mirror_url) could not list repos!", $cleanup ) unless @fetched;
+        push(@repos_mirror, @fetched);
     }
 
-	# Clean up
+	# Build a map of names to clone URIs
+	my $field_name = $options{nossh} ? 'clone_url' : 'ssh_url';
+	my %names2clone = map { $_->{name} => $_->{$field_name} } @repos_local;
+	my %mirror_uris;
+	foreach my $repo (@repos_mirror) {
+		$mirror_uris{$repo->{name}} //= [];
+		push( @{$mirror_uris{$repo->{name}}}, $repo->{$field_name});
+	}
+
 	$cleanup->();
 
     use Data::Dumper;
-    die Dumper(\%repos_mirror, \@repos_local);
+    die Dumper(\%names2clone, \%mirror_uris);
+
+	my @to_create;
+	foreach my $to_clone (keys(%names2clone)) {
+		my $res = Git::command_oneline([ 'clone', $names2clone{$to_clone} ]);
+		my $repo = Git->repository(Directory => $to_clone);
+
+		#TODO check privacy field
+
+		if (!exists $mirror_uris{$to_clone}) {
+			#TODO push into @to_create if $create
+			next;
+		}
+		# TODO check if the repo is missing on just *some* of the mirrors and add to @to_create if $create
+
+		foreach my $mirror (@{$mirror_uris{$to_clone}}) {
+			# The real "magic" here -- you can have many push urls.
+			$repo->command(qw{git remote set-url --add --push}, 'origin', $mirror);
+			#TODO make sure they are all synced up if --sync
+		}
+	}
+
+	# Clean up
+	$cleanup->();
+	return 0;
 }
 
 sub _cleanup_token {
 	my ( $api, $password, $insecure ) = @_;
-
-	my $tok = $api->token();
-	# unset the token, so that we use simple auth once more
-	$api->token("");
-
-	my $result = $api->delete_token( sha1 => $tok, password => $password, insecure => $insecure );
-
+	my $result = $api->delete_token( sha1 => $api->token, password => $password, insecure => $insecure );
 	die "Could not clean up token" unless $result && $result->response->is_success;
 }