git-clone-entity 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/env perl
  2. package Git::CloneEntity;
  3. use strict;
  4. use warnings;
  5. use FindBin::libs;
  6. use Getopt::Long qw{GetOptionsFromArray};
  7. use Pod::Usage;
  8. use Pithub;
  9. use Gogs;
  10. use Term::ReadKey();
  11. use IO::Interactive::Tiny();
  12. =head1 DESCRIPTION
  13. 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.
  14. Currently (AD 2024), and for the forseeable future, github.com will be such a platform.
  15. It is also a common pattern to need to clone basically everything for a given user/org when new development environments are instantiated.
  16. Alternatively, you may just want to keep your local development environment up to date for said users/projects.
  17. 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).
  18. It will warn you whenever a repository is missing from either, so you can make it go whirr appropriately.
  19. Using this you can easily migrate an organization from being entirely on github to using private resources or vice versa.
  20. =head1
  21. =head1 USAGE
  22. 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]
  23. =cut
  24. sub _help {
  25. my ($code, $msg, $cb) = @_;
  26. $code //= 0;
  27. $msg //= "";
  28. $cb->() if ref $cb eq 'CODE';
  29. return Pod::Usage::pod2usage( -message => $msg, -exitval => $code);
  30. }
  31. my $domainRipper = qr{^\w+://([\w|\.]+)};
  32. sub main {
  33. my @args = @_;
  34. my $help;
  35. my ($users, $orgs, $aliases, $tokens, $mirrors, $baseurl, $gogs, $me, $insecure) = ([],[],[],[],[],"", 0, "", 0);
  36. GetOptionsFromArray(\@args,
  37. 'me=s' => \$me,
  38. 'user=s@' => \$users,
  39. 'alias=s@' => \$aliases,
  40. 'token=s@' => \$tokens,
  41. 'org=s@' => \$orgs,
  42. 'baseurl=s' => \$baseurl,
  43. 'mirror=s@' => \$mirrors,
  44. 'gogs' => \$gogs,
  45. 'insecure' => \$insecure,
  46. 'help' => \$help,
  47. );
  48. return _help() if $help;
  49. return _help(1, "Must pass at least one user or organization") unless (@$users + @$orgs);
  50. return _help(2, "Must pass baseurl") unless $baseurl;
  51. return _help(3, "Must pass your username as --me") unless $me;
  52. # Parse Alias mappings
  53. my %alias_map;
  54. if (@$aliases) {
  55. foreach my $arg (@$aliases) {
  56. my ($actual, $domain, $alias) = split(/:/, $arg);
  57. return _help(3, "aliases must be of the form user:domain:alias") unless $actual && $domain && $alias;
  58. $alias_map{$domain}{$actual} = $alias;
  59. }
  60. }
  61. my ($primary_domain) = $baseurl =~ $domainRipper;
  62. my %tokens;
  63. foreach my $tok (@$tokens) {
  64. my ($domain, $token) = split(/:/, $tok);
  65. return _help(4, "tokens must be of the form domain:token") unless $domain && $token;
  66. $tokens{$domain} = $token;
  67. }
  68. my $primary_token = $tokens{$primary_domain};
  69. my %args = (
  70. user => $me,
  71. api_uri => $baseurl,
  72. );
  73. $args{token} = $primary_token if $primary_token;
  74. # It's important which is the primary, because we can have only one pull url, and many push urls.
  75. my $local = $gogs ? Gogs->new(%args) : Pithub->new( %args );
  76. # If the primary is gogs and we have no token passed, let's make one.
  77. my $password;
  78. if (!$primary_token && $gogs) {
  79. _help(5, "Program must be run interactively to auto-create keys on Gogs installs.") unless IO::Interactive::Tiny::is_interactive();
  80. # Stash the password in case we gotta clean up
  81. $password = _prompt("Please type in the password for ".$local->user.":");
  82. $primary_token = $local->get_token(
  83. name => "git-clone-entity",
  84. password => $password,
  85. insecure => $insecure,
  86. );
  87. _help(6, "Could not fetch token from gogs! Check that you supplied the correct username & password.") unless $primary_token;
  88. $local->token($primary_token);
  89. }
  90. my $cleanup = sub { _cleanup_token( $local, $password, $insecure ) if $password };
  91. # TODO XXX this is not appending /api/v1 for some reason
  92. my @repos_local = _fetch_all($local, $users, $orgs);
  93. _help(7, "Server at $baseurl could not list repos!", $cleanup ) unless @repos_local;
  94. my %repos_mirror;
  95. foreach my $mirror_url (@$mirrors) {
  96. my ($mirror_domain) = $mirror_url =~ $domainRipper;
  97. my $muser = $me;
  98. $muser = $alias_map{$mirror_domain}{$muser} if exists $alias_map{$mirror_domain}{$muser};
  99. my %margs = (
  100. user => $muser,
  101. api_uri => $mirror_url,
  102. );
  103. $args{token} = $tokens{$mirror_domain} if $tokens{$mirror_domain};
  104. my $mirror = Pithub->new( api_uri => $mirror_url );
  105. $repos_mirror{$mirror_url} = _fetch_all($mirror, $users, $orgs, \%alias_map);
  106. _help(8, "The provided mirror ($mirror_url) could not list repos!", $cleanup ) unless @{$repos_mirror{$mirror_url}};
  107. }
  108. # Clean up
  109. $cleanup->();
  110. use Data::Dumper;
  111. die Dumper(\%repos_mirror, \@repos_local);
  112. }
  113. sub _cleanup_token {
  114. my ( $api, $password, $insecure ) = @_;
  115. my $tok = $api->token();
  116. # unset the token, so that we use simple auth once more
  117. $api->token("");
  118. my $result = $api->delete_token( sha1 => $tok, password => $password, insecure => $insecure );
  119. die "Could not clean up token" unless $result && $result->response->is_success;
  120. }
  121. sub _prompt {
  122. my ( $prompt ) = @_;
  123. $prompt ||= "";
  124. my $input = "";
  125. print $prompt;
  126. # We are readin a password
  127. Term::ReadKey::ReadMode('noecho');
  128. {
  129. local $SIG{'INT'} = sub { Term::ReadKey::ReadMode(0); exit 130; };
  130. $input = <STDIN>;
  131. chomp($input) if $input;
  132. }
  133. Term::ReadKey::ReadMode(0);
  134. print "\n";
  135. return $input;
  136. }
  137. sub _fetch_all {
  138. my ($api, $users, $orgs, $alias_map) = @_;
  139. my ($domain) = $api->api_uri =~ $domainRipper;
  140. my @repos;
  141. foreach my $user (@$users) {
  142. $user = $alias_map->{$domain}{$user} if exists $alias_map->{$domain}{$user};
  143. my $result = $api->repos->list( user => $user );
  144. push(@repos, _array_content($result));
  145. }
  146. foreach my $org (@$orgs) {
  147. $org = $alias_map->{$domain}{$org} if exists $alias_map->{$domain}{$org};
  148. my $result = $api->repos->list( org => $org );
  149. push(@repos, _array_content($result));
  150. }
  151. return @repos;
  152. }
  153. sub _array_content {
  154. my ($result) = @_;
  155. return () unless $result && $result->response->is_success;
  156. return @{$result->content()} if ref $result->content() eq 'ARRAY';
  157. return ();
  158. }
  159. exit main(@ARGV) unless caller;
  160. 1;