George Baugh 1 rok temu
commit
35ccb72822
3 zmienionych plików z 259 dodań i 0 usunięć
  1. 7 0
      Readme.md
  2. 172 0
      bin/git-clone-entity
  3. 80 0
      lib/Gogs.pm

+ 7 - 0
Readme.md

@@ -0,0 +1,7 @@
+# Gogs for perl
+
+Simple wrapper around Pithub.pm to interact with the Gogs api
+
+#git clone-entity
+
+Ability to clone entire users/orgs reposets

+ 172 - 0
bin/git-clone-entity

@@ -0,0 +1,172 @@
+#!/usr/bin/env perl
+
+package Git::CloneEntity;
+
+use strict;
+use warnings;
+
+use FindBin::libs;
+
+use Getopt::Long qw{GetOptionsFromArray};
+use Pod::Usage;
+use Pithub;
+use Gogs;
+
+use Term::ReadKey();
+use IO::Interactive::Tiny();
+
+=head1 DESCRIPTION
+
+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).
+
+It will warn you whenever a repository is missing from either, so you can make it go whirr appropriately.
+
+Using this you can easily migrate an organization from being entirely on github to using private resources or vice versa.
+
+=head1
+
+=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]
+
+=cut
+
+sub _help {
+    my ($code, $msg) = @_;
+    $code //= 0;
+    $msg //= "";
+    return Pod::Usage::pod2usage( -message => $msg, -exitval => $code);
+}
+
+my $domainRipper = qr{m/^\w+://([\w|\.]+)};
+
+sub main {
+    my @args = @_;
+
+    my $help;
+    my ($users, $orgs, $aliases, $tokens, $mirrors, $baseurl, $gogs, $me) = ([],[],[],[],[],"", 0, "");
+
+    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,
+        'help'      => \$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") unless $me;
+
+    # Parse Alias mappings
+    my %alias_map;
+    if (@$aliases) {
+        foreach my $arg (@$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 %tokens;
+    foreach my $tok (@$tokens) {
+        my ($domain, $token) = split(/:/, $tok);
+        return _help(4, "tokens must be of the form domain:token") unless $domain && $token;
+        $tokens{$domain} = $token;
+    }
+
+    $primary_token = $tokens{$primary_domain};
+    my %args = (
+        user    => $me,
+        api_uri => $baseurl,
+        token   => $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 );
+
+    # If the primary is gogs and we have no token passed, let's make one.
+	if (!$primary_token && $gogs) {
+		_help(5, "Program must be run interactively to auto-create keys on Gogs installs.") unless IO::Interactive::Tiny::is_interactive();
+		$primary_token = $local->get_token(
+			name     => "git-clone-entity",
+			password => _prompt("Please type in the password for $local->user:"),
+		);
+	}
+
+    my @repos_local = _fetch_all($local, $users, $orgs);
+
+    my %repos_mirror;
+    foreach my $mirror_url (@$mirrors) {
+        my ($mirror_domain) = $mirror_url =~ $domainRipper;
+        my $muser = $me;
+        $muser = $alias_map->{$domain}{$me} if exists $alias_map->{$domain}{$me};
+        my %margs = (
+            user    => $muser,
+            api_uri => $mirror_url,
+            token   => $tokens{$mirror_domain},
+        );
+
+        my $mirror = Pithub->new( api_uri => $mirror_url );
+        $repos_mirror{$mirror_url} = _fetch_all($mirror, $users, $orgs, \%alias_map);
+    }
+
+    use Data::Dumper;
+    die Dumper(\%repos_mirror, \@repos_local, \%alias_map);
+}
+
+sub _prompt {
+	my ( $prompt ) = @_;
+    $prompt ||= "";
+    my $input = "";
+
+    print $prompt;
+
+	# We are readin a password
+    Term::ReadKey::ReadMode('noecho');
+    {
+    	local $SIG{'INT'} = sub { Term::ReadKey::ReadMode(0); exit 130; };
+        $input = <STDIN>;
+        chomp($input) if $input;
+    }
+    Term::ReadKey::ReadMode(0);
+    print "\n";
+    return $input;
+}
+
+sub _fetch_all {
+    my ($api, $users, $orgs, $alias_map) = @_;
+
+    my ($domain) = $api->api_uri =~ $domainRipper;
+
+    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, $result) if $result;
+    }
+    foreach my $org (@$orgs) {
+        $org = $alias_map->{$domain}{$org} if exists $alias_map->{$domain}{$org};
+        my $result = $api->repos->list( org => $org );
+        push(@repos, $result) if $result;
+    }
+    return @repos;
+ }
+
+exit main(@ARGV) unless caller;
+
+1;
+

+ 80 - 0
lib/Gogs.pm

@@ -0,0 +1,80 @@
+package Gogs;
+
+# ABSTRACT: Subclass of Pithub
+use strict;
+use warnings;
+
+use Moo;
+use Mime::Base64;
+
+extends 'Pithub';
+
+=head1 DESCRIPTION
+
+L<Pithub> is an acceptable API client for Gogs, since their APIs are compatible.
+
+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.
+
+=head1 SUBROUTINES
+
+=head2 get_token(%options)
+
+Gogs has no user-facing means of creating tokens like github does.
+You have to POST against the API using HTTP simple auth to acquire them.
+
+Requires that the object is instantiated with a username,
+that you pass a password, and that the api_uri is secure unless you pass the 'insecure' option.
+
+Will either fetch (if it exists) or create the named token.
+
+=cut
+
+# All API access requires a token on Gogs EXCEPT users/$user/tokens.
+# As such, I'm restricting every call not ending in /tokens
+my @TOKEN_REQUIRED_REGEXP = (qr{(?!/tokens$)});
+
+sub get_token {
+    my ($self, %options) = @_;
+
+    die "Must instantiate with username" unless $self->user;
+    die "Must pass token name" unless $options{name};
+    die "Refuse to operate over insecure connections" if !$options{insecure} && _insecure($uri);
+    die "Must provide password to fetch tokens" unless $options{password};
+
+    my %auth_headers = (
+        'Authorization' => "Basic ".encode_base64("$self->user:$options{password}"),
+        'Content-type'  => "application/json",
+    );
+
+    my $token_endpoint = "/api/v1/users/$self->user/tokens";
+    my %req = (
+         method  => 'GET',
+        path    => $tokens_endpoint,
+        headers => \%auth_headers,
+    );
+    my $result = $self->request(%req);
+    my $content = $result->content();
+    die "Got bad response from server" unless ref $content eq "ARRAY";
+
+    my $token_actual;
+    foreach my $token (@$content) {
+        next unless $token->{name} eq $options{name};
+        return $token->{sha1};
+    }
+
+    # No such token named, so let's just make one.
+    $req{method} = 'POST';
+    my $result = $self->request(%req);
+    my $content = $result->content();
+    die "Got bad response from server" unless ref $content eq "HASH";
+    return $content->{sha1};
+}
+
+sub _insecure {
+    my $uri = shift;
+    return m/^http:\/\//;
+}
+
+1;