Explorar o código

Yet more complication aaaaah

George Baugh hai 1 ano
pai
achega
939cc3f383
Modificáronse 2 ficheiros con 143 adicións e 118 borrados
  1. 141 118
      bin/git-clone-entity
  2. 2 0
      lib/Gogs.pm

+ 141 - 118
bin/git-clone-entity

@@ -7,6 +7,7 @@ use warnings;
 
 use FindBin::libs;
 
+use HTTP::Tiny;
 use Config::Simple;
 use Getopt::Long qw{GetOptionsFromArray};
 use Pod::Usage;
@@ -21,12 +22,20 @@ use IO::Interactive::Tiny();
 
 It is a common pattern in organizations to have their own git resources, but mirror everything public on one of the big platforms with network effect.
 
-Currently (AD 2024), and for the forseeable future, github.com will be such a platform.
-
 It is also a common pattern to need to clone basically everything for a given user/org when new development environments are instantiated.
 Alternatively, you may just want to keep your local development environment up to date for said users/projects.
 
-This program facilitiates cloning your (public) repositories for given users/orgs from either a local gogs/github instance and configuring pushurls for both it and github, or any other github-api compatible mirror(s).
+This program facilitiates cloning your repositories for given users/orgs from either a local gogs/github instance and configuring pushurls for both it and github, or any other github-api compatible mirror(s).
+
+By default it will configure your origin remote to fetch from the baseurl provided, and push to it and the mirror(s) provided, but you can specify whatever remotename ('all' seems popular) you desire.
+Regardless, remotes for the base and mirrors will also be set up in case individual pushes must be made.
+
+In the event that two different users/orgs have the same named repository (e.g. forks) it will .
+Will set up remotes named after the user/org in the event the repo is a fork, and set the 'upstream' name to be the parent repository.
+This will not recursively scan for the oldest ancestor as parent; most of the time that's a bad idea.
+
+In the event that all the copies of a repo happen to be a fork on the passed users/orgs,
+whatever the --primary_user or --primary_org will be preferred.
 
 It will warn you whenever a repository is missing from either, so you can make it go whirr appropriately.
 
@@ -43,7 +52,6 @@ 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
@@ -54,7 +62,7 @@ The name of the setting will be pluralized for any option which may be passed mu
 
 =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] [--nossh] [--insecure] [--help]
+git clone-entity --user $user1 --user $user2 --org $org1 --org $org2 --alias $user1:$mirror_domain:$mirrorUser1 --baseurl=https://my.local.install/ [--mirror https://github.com] [--nossh] [--insecure] [--help]
 
 =head1 OPTIONS
 
@@ -71,14 +79,6 @@ 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.
@@ -93,6 +93,14 @@ Token for a particular baseurl or mirror.  Of the format domain:token.
 
 You can omit the auth token on gogs, as we can create them automatically (we will prompt for your password).
 
+=head3 primary_user, primary_org
+
+Primary entity to clone.  Consider their repository to be the canonical one.  One or the other must be passed.  In the event both are, the org is preferred.
+
+In most organizations, you will have the org hold the primary copy of a repo, with developers forking copies.
+
+	--primary_org 'BigHugsLLC'
+
 =head3 user
 
 Clone all of this user's repositories.  May be passed multiple times.
@@ -119,6 +127,13 @@ Don't use SSH clone URIs.  Useful for read-only clones & deployments with no ssh
 
 	--nossh
 
+=head3 remote
+
+Name of primary remote.  By default will be 'origin', but 'all' is popular.
+In the event this is not origin, origin will be set to be the push/pull for the repo at the baseurl.
+
+	--remote all
+
 =head1 CONSEQUENTIAL OPTIONS
 
 =head3 insecure
@@ -169,9 +184,11 @@ sub main {
 		me       => undef,
 		create   => undef,
 		sync     => undef,
-		gogs     => undef,
 		insecure => undef,
 		nossh    => undef,
+		remote   => 'origin',
+		primary_user  => undef,
+		primary_org   => undef,
 	);
 
 	# Allow options to override configuration
@@ -203,28 +220,33 @@ sub main {
         'org=s@'    => \$options{orgs},
         'baseurl=s' => \$options{baseurl},
         'mirror=s@' => \$options{mirrors},
-        'gogs'      => \$options{gogs},
         'insecure'  => \$options{insecure},
 		'nossh'     => \$options{nossh},
         'help'      => \$options{help},
+		'primary_user=s' => \$options{primary},
+		'primary_org=s'  => \$options{primary_org},
     );
 
+	# Tiebreaker vote in the event of conflicting forks
+	push(@{$options{users}}, $options{primary_user}) if $options{primary_user};
+	push(@{$options{orgs}},  $options{primary_org})  if $options{primary_org};
+	my $prime_name = $options{primary_org} || $options{primary_user};
+
     return _help() if $options{help};
-    return _help(1, "Must pass at least one user or organization") unless (@{$options{users}} + @{$options{orgs}});
+	return _help(1, "Must pass either primary_user or primary_org") unless $prime_name;
+    return _help(1, "Must pass at least one of: user or org") 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 (@{$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;
-        }
-    }
+	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) = $options{baseurl} =~ $domainRipper;
+	# Parse tokens
     my %tokens;
     foreach my $tok (@{$options{tokens}}) {
         my ($domain, $token) = split(/:/, $tok);
@@ -232,38 +254,19 @@ sub main {
         $tokens{$domain} = $token;
     }
 
-    my $primary_token = $tokens{$primary_domain};
-    my %args = (
-        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  = $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 && $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 => $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);
-	}
+	# Simplify code below by making the primary just another mirror to fetch
+	unshift(@{$options{mirrors}}, $options{baseurl});
+
+	my $field_name = $options{nossh} ? 'clone_url' : 'ssh_url';
 
-	my $cleanup = sub { _cleanup_token( $local, $password, $options{insecure} ) if $password };
+    my @repos;
+	my (%passwords, %clients);
 
-    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 $cleanup = sub { _cleanup_tokens( \%clients, \%passwords, $options{insecure} ) if %passwords };
 
-    my @repos_mirror;
     foreach my $mirror_url (@{$options{mirrors}}) {
+		my $server_is_github = _server_is_github($mirror_url);
+
         my ($mirror_domain) = $mirror_url =~ $domainRipper;
         my $muser = $options{me};
         $muser = $alias_map{$mirror_domain}{$muser} if exists $alias_map{$mirror_domain}{$muser};
@@ -271,78 +274,66 @@ sub main {
             user    => $muser,
             api_uri => $mirror_url,
         );
-        $args{token} = $tokens{$mirror_domain} if $tokens{$mirror_domain};
-
-        my $mirror = Pithub->new( api_uri => $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);
-    }
-
-	# 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;
-
-	# Figure out which repos are private so we don't mirror them unless instructed to.
-	my @private_repos = map { $_->{name} } grep { $_->{private} } @repos_local;
-
-	# Grab all the mirrors push uris
-	my %mirror_uris;
-	foreach my $repo (@repos_mirror) {
-		$mirror_uris{$repo->{name}} //= [];
-		push( @{$mirror_uris{$repo->{name}}}, $repo->{$field_name});
-	}
-
-	foreach my $to_clone (keys(%names2clone)) {
-		#XXX testing removme
-		next unless $to_clone eq 'perl-Gogs';
-
-		# Don't clone it if it is already present.
-		my $already_exists=1;
-		if (!-d $to_clone) {
-			$already_exists=0;
-			my $res = Git::command_oneline([ 'clone', $names2clone{$to_clone} ]);
+        $margs{token} = $tokens{$mirror_domain} if $tokens{$mirror_domain};
+
+        my $mirror = $server_is_github ? Pithub->new(%margs) : Gogs->new(%margs);
+
+		# Then it's gogs, and we can just make one.
+		if (!$margs{token} && !$server_is_github) {
+			_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
+			$passwords{$mirror_domain} = _prompt("Please type in the password for ".$mirror->user.":");
+			$tokens{$mirror_domain} = $mirror->get_token(
+				name     => "git-clone-entity",
+				password => $passwords{$mirror_domain},
+				insecure => $options{insecure},
+			);
+			_help(6, "Could not fetch token from gogs!  Check that you supplied the correct username & password.") unless $tokens{$mirror_domain};
+			$mirror->token($tokens{$mirror_domain});
+			# Stash for later use by cleanup routines if needed
+			$clients{$mirror_domain} = $mirror;
 		}
-		my $repo = Git->repository(Directory => $to_clone);
 
-		# Figure out what the remotes look like
-		my $res = $repo->command(qw{remote -v});
-		my %remotes = _parse_remotes($res);
+		my @fetched = _fetch_all($mirror, $options{users}, $options{orgs}, \%alias_map, $field_name);
+        _help(7, "The provided server ($mirror_url) could not list repos!", $cleanup ) unless @fetched;
 
-		# TODO add specific remotes for the baseurl and mirrors.  This way we can manually push to each if we need to sync.
+        push(@repos, @fetched);
+    }
 
-		# Ensure that origin is correctly the local one for those that already exist
-		if ($remotes{origin}{fetch} ne $names2clone{$to_clone}) {
-			print "Incorrect origin in $to_clone, correcting...\n";
-			$res = $repo->command(qw{remote set-url --fetch origin}, $names2clone{$to_clone});
-			# TODO error handling
+	#TODO actually differentiate between the various clones, build username/org remotes
 
-			# We don't care if push uri already there
-			next if any { $_ eq $names2clone{$to_clone} } @{$remotes{origin}{push}};
+	# Build a map of names to clone URIs
+	my %names2clone = map { $_->{name} => $_->{$field_name} } @repos;
+	my %upstreams   = map { $_->{name} => $_->{upstream_uri} } @repos;
 
-			print "Lacking push URI for $to_clone, correcting...\n";
-			$res = $repo->command(qw{remote set-url --add --push origin}, $names2clone{$to_clone});
-			#TODO error handling
-		}
-		# TODO figure out which mirrors are missing, and add them if needed (consider privacy)
-
-		foreach my $mirror_uri ($mirror_uris{$to_clone}) {
-			# We don't care if it's already there
-			next if any { $_ eq $mirror_uri } @{$remotes{origin}{push}};
-			next if !$options{private} && any { $_ eq $to_clone } @private_repos;
-			print "Lacking push URI $mirror_uri, adding to origin...\n";
-			$res = $repo->command(qw{remote set-url --add --push origin}, $mirror_uri);
-			#TODO error handling
-		}
+	# Figure out which repos are private so we don't mirror them unless instructed to.
+	my @private_repos = map { $_->{name} } grep { $_->{private} } @repos;
 
-		# Finally, sync up the mirrors if instructed.  This is important, as push URIs which aren't in sync will leave git in an inconsistent state.
-	}
+	$cleanup->();
+	use Data::Dumper;
+	die Dumper(\%names2clone, \%upstreams, \@private_repos);
 
 	# Clean up
 	$cleanup->();
 	return 0;
 }
 
+sub _clone_repos {
+}
+
+sub _fetch_upstream_uri {
+	my ($mirror, $field_name, $muser, $repo) = @_;
+	my $upstream_uri;
+	if ($repo->{fork}) {
+		my $details = $mirror->repos->get( user => $muser, repo => $repo->{name});
+		_help(9, "Could not fetch repository details for $repo->{name}") unless $details && $details->response->is_success();
+		my $content = $details->content();
+		$upstream_uri = $content->{parent}{$field_name};
+		_help(10, "Could not discern upstream URI for forked repo $repo->{name}!") unless $upstream_uri;
+	}
+	return $upstream_uri;
+}
+
 sub _parse_remotes {
 	my ($raw) = shift;
 	my %parsed;
@@ -358,10 +349,13 @@ sub _parse_remotes {
 	return %parsed;
 }
 
-sub _cleanup_token {
-	my ( $api, $password, $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;
+sub _cleanup_tokens {
+	my ( $apis, $passwords, $insecure ) = @_;
+	foreach my $domain (keys(%$apis)) {
+		my $api = $apis->{$domain};
+		my $result = $api->delete_token( sha1 => $api->token, password => $passwords->{$domain}, insecure => $insecure );
+		die "Could not clean up token" unless $result && $result->response->is_success;
+	}
 }
 
 sub _prompt {
@@ -384,20 +378,30 @@ sub _prompt {
 }
 
 sub _fetch_all {
-    my ($api, $users, $orgs, $alias_map) = @_;
+    my ($api, $users, $orgs, $alias_map, $field_name) = @_;
 
     my ($domain) = $api->api_uri =~ $domainRipper;
 
+	# TODO detect which repo among forks is the "primary" (if one of them is not a fork, use it)
+
     my @repos;
     foreach my $user (@$users) {
         $user = $alias_map->{$domain}{$user} if exists $alias_map->{$domain}{$user};
         my $result = $api->repos->list( user => $user );
-        push(@repos, _array_content($result));
+
+		my @fetched = _array_content($result);
+		@fetched    = _augment_repos($api, $field_name, $user, $domain, @fetched);
+
+        push(@repos, @fetched);
     }
     foreach my $org (@$orgs) {
         $org = $alias_map->{$domain}{$org} if exists $alias_map->{$domain}{$org};
         my $result = $api->repos->list( org => $org );
-        push(@repos, _array_content($result));
+
+		my @fetched = _array_content($result);
+		@fetched    = _augment_repos($api, $field_name, $org, $domain, @fetched);
+
+        push(@repos, @fetched);
     }
     return @repos;
  }
@@ -409,6 +413,25 @@ sub _array_content {
 	return ();
 }
 
+sub _augment_repos {
+	my ($mirror, $field_name, $muser, $domain, @fetched) = @_;
+	@fetched = map {
+		my $subj = $_;
+		$subj->{domain} = $domain;
+		$subj->{upstream_uri} = _fetch_upstream_uri($mirror, $field_name, $muser, $subj);
+		$subj
+	} @fetched;
+	return @fetched;
+}
+
+sub _server_is_github {
+	my ($uri) = @_;
+	my $ua = HTTP::Tiny->new();
+	my $res = $ua->get($uri);
+	# GOGS will 404 it's api baseurl, github will not
+	return $res->{success};
+}
+
 exit main(@ARGV) unless caller;
 
 1;

+ 2 - 0
lib/Gogs.pm

@@ -18,6 +18,8 @@ However the two have a number of differences, so they must be accounted for.
 
 The most important of which being that all requests require an API token.
 
+It is the caller's responsibility to know ahead of time whether the server is gogs or not.
+
 =head1 METHODS
 
 =head2 get_token(%options)