git-clone-entity 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. #!/usr/bin/env perl
  2. package Git::CloneEntity;
  3. use strict;
  4. use warnings;
  5. use FindBin::libs;
  6. use List::Util qw{first any uniq all};
  7. use HTTP::Tiny;
  8. use Config::Simple;
  9. use Getopt::Long qw{GetOptionsFromArray};
  10. use Pod::Usage;
  11. use Pithub;
  12. use Gogs;
  13. use Git;
  14. use Clone qw{clone};
  15. use Term::ReadKey();
  16. use IO::Interactive::Tiny();
  17. use Data::Dumper;
  18. $Data::Dumper::Purity=1;
  19. $Data::Dumper::Deepcopy=1;
  20. =head1 DESCRIPTION
  21. 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.
  22. It is also a common pattern to need to clone basically everything for a given user/org when new development environments are instantiated.
  23. Alternatively, you may just want to keep your local development environment up to date for said users/projects.
  24. 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).
  25. It will configure your 'origin' & 'upstream' remote to fetch from the baseurl provided, and push to it and the mirror(s) provided.
  26. Regardless, remotes for the users/orgs provided will also be set up in case individual pushes must be made.
  27. In the event that two different users/orgs have the same named repository (e.g. forks) it
  28. 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.
  29. This will not recursively scan for the oldest ancestor as parent; most of the time that's a bad idea.
  30. In the event that all the copies of a repo happen to be a fork on the passed users/orgs,
  31. whatever the --primary_user or --primary_org will be preferred.
  32. It will warn you whenever a repository is missing from either, so you can make it go whirr appropriately.
  33. Using this you can easily migrate an organization from being entirely on github to using private resources or vice versa.
  34. =head1 IMPORTANT
  35. This assumes that the repo names between the base and mirrors is identical.
  36. =head1 CONFIG FILE
  37. You will notice below that the options of this tool can be quite involved.
  38. To simplify deploying this tool across your organization, you can place a configuration file (Config::Simple) in ~/.git/clone-entity.cfg. Example:
  39. baseurl=https://my-gogs-install.test/api/v1
  40. nossh=true
  41. mirrors=https://api.github.com,https://premise-install.github.local/api
  42. me=jane
  43. Ideally all your users have to do is specify which users/orgs to clone w/mirroring and you should be off to the races.
  44. The name of the setting will be pluralized for any option which may be passed multiple times below.
  45. =head1 USAGE
  46. 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]
  47. =head1 OPTIONS
  48. =head3 me
  49. Your username on the baseurl. Relevant to token use, what is visible, etc.
  50. All this users' repositories will be cloned. Additional users' repos will be added as remotes if your interests intersect.
  51. --me tarzan
  52. =head3 baseurl
  53. URI for your Git management solution. Currently github and gogs are supported.
  54. --baseurl https://api.github.com
  55. --baseurl https://gogs.mydomain.test/api/v1
  56. =head3 mirror
  57. URI for a git management solution you wish to use for mirroring the repos at the baseurl. May be passed multiple times.
  58. --mirror https://on-prem.github.local/api/
  59. =head3 token
  60. Token for a particular baseurl or mirror. Of the format domain:token.
  61. --token my.domain.test:DEADBEEF
  62. You can omit the auth token on gogs, as we can create them automatically (we will prompt for your password).
  63. =head3 primary_user, primary_org
  64. Primary entity to clone. Defaults to --me in the event neither are passed. Considers their repository to be the canonical one. In the event both are, the org is preferred.
  65. In most organizations, you will have the org hold the primary copy of a repo, with developers forking copies. This will become the "upstream" remote.
  66. --primary_org 'BigHugsLLC'
  67. =head3 user
  68. Add remotes for this user's repositories if interests intersect. May be passed multiple times.
  69. --user fred
  70. =head3 org
  71. Add remotes for this org's repositories if interests intersect. May be passed multiple times.
  72. --org 'Granite-Industries'
  73. =head3 alias
  74. Map a user/org on your baseurl to a mirror. Of the format base_user:mirror_domain:mirror_user.
  75. Obviously won't work if the mirror is on the same hostname as the baseurl; use a subdomain at the very least.
  76. Also used to alias --me in the case it's different on the primary and mirrors.
  77. --alias george:sprockets.spacely.local:gjetson
  78. =head3 nossh
  79. Don't use SSH clone URIs. Useful for read-only clones & deployments with no ssh-agent.
  80. --nossh
  81. =head3 remote
  82. Name of primary remote. By default will be 'origin', but 'all' is popular.
  83. In the event this is not origin, origin will be set to be the push/pull for the repo at the baseurl.
  84. --remote all
  85. =head1 CONSEQUENTIAL OPTIONS
  86. =head3 insecure
  87. Allow insecure mirrors or baseurls. This is just to prevent footgunning by passing auth over plaintext.
  88. --insecure
  89. =head3 create
  90. Automatically create a copy of the repo on the mirror or baseurl if it doesn't exist.
  91. --create
  92. =head3 private
  93. If --create is passed, also mirror repositories marked as private, preserving privacy.
  94. =head3 sync
  95. Force push all refs onto the mirror(s).
  96. --sync
  97. =cut
  98. sub _help {
  99. my ($code, $msg, $cb) = @_;
  100. $code //= 0;
  101. $msg //= "";
  102. $cb->() if ref $cb eq 'CODE';
  103. return Pod::Usage::pod2usage( -message => $msg, -exitval => $code);
  104. }
  105. my $domainRipper = qr{^\w+://([\w|\.]+)};
  106. my $verbose;
  107. sub LOG {
  108. print shift."\n" if $verbose;
  109. }
  110. sub main {
  111. my @args = @_;
  112. my %options = (
  113. help => undef,
  114. users => [],
  115. orgs => [],
  116. aliases => [],
  117. tokens => [],
  118. mirrors => [],
  119. baseurl => "",
  120. me => undef,
  121. create => undef,
  122. sync => undef,
  123. insecure => undef,
  124. nossh => undef,
  125. remote => 'origin',
  126. primary_user => undef,
  127. primary_org => undef,
  128. verbose => undef,
  129. );
  130. # Allow options to override configuration
  131. my $home = $ENV{HOME};
  132. mkdir "$home/.git" unless -d "$home/.git";
  133. my $config_file = "$home/.git/clone-entity.cfg";
  134. if (-f $config_file) {
  135. my $conf = Config::Simple->new($config_file);
  136. my %config;
  137. %config = %{$conf->param(-block => 'default')} if $conf;
  138. # Merge the configuration with the options
  139. foreach my $opt (keys(%options)) {
  140. if ( ref $options{$opt} eq 'ARRAY' ) {
  141. next unless exists $config{$opt};
  142. my @arrayed = ref $config{$opt} eq 'ARRAY' ? @{$config{$opt}} : ($config{$opt});
  143. push(@{$options{$opt}}, @arrayed);
  144. next;
  145. }
  146. $options{$opt} = $config{$opt} if exists $config{$opt};
  147. }
  148. }
  149. GetOptionsFromArray(\@args,
  150. 'me=s' => \$options{me},
  151. 'user=s@' => \$options{users},
  152. 'alias=s@' => \$options{aliases},
  153. 'token=s@' => \$options{tokens},
  154. 'org=s@' => \$options{orgs},
  155. 'baseurl=s' => \$options{baseurl},
  156. 'mirror=s@' => \$options{mirrors},
  157. 'insecure' => \$options{insecure},
  158. 'nossh' => \$options{nossh},
  159. 'help' => \$options{help},
  160. 'primary_user=s' => \$options{primary},
  161. 'primary_org=s' => \$options{primary_org},
  162. 'verbose' => \$options{verbose},
  163. 'create' => \$options{create},
  164. 'sync' => \$options{sync},
  165. );
  166. $verbose = $options{verbose};
  167. $options{primary_user} = $options{me} if !$options{primary_user} || !$options{primary_org};
  168. # Tiebreaker vote in the event of conflicting forks
  169. push(@{$options{users}}, $options{primary_user}) if $options{primary_user};
  170. push(@{$options{orgs}}, $options{primary_org}) if $options{primary_org};
  171. push(@{$options{users}}, $options{me}) if $options{me};
  172. my $prime_name = $options{primary_org} || $options{primary_user};
  173. return _help() if $options{help};
  174. return _help(1, "Must pass either primary_user or primary_org") unless $prime_name;
  175. return _help(1, "Must pass at least one of: user or org") unless (@{$options{users}} + @{$options{orgs}});
  176. return _help(2, "Must pass baseurl") unless $options{baseurl};
  177. return _help(3, "Must pass your username as --me") unless $options{me};
  178. # Parse Alias mappings
  179. my (%alias_map, %alias_reverse);
  180. foreach my $arg (@{$options{aliases}}) {
  181. my ($actual, $domain, $alias) = split(/:/, $arg);
  182. return _help(3, "aliases must be of the form user:domain:alias") unless $actual && $domain && $alias;
  183. $alias_map{$domain}{$actual} = $alias;
  184. $alias_reverse{$domain}{$alias} = $actual;
  185. }
  186. # Parse tokens
  187. my %tokens;
  188. foreach my $tok (@{$options{tokens}}) {
  189. my ($domain, $token) = split(/:/, $tok);
  190. return _help(4, "tokens must be of the form domain:token") unless $domain && $token;
  191. $tokens{$domain} = $token;
  192. }
  193. # Simplify code below by making the primary just another mirror to fetch
  194. my @mirror_domains = map {
  195. my $subj = $_;
  196. my ($dom) = $subj =~ $domainRipper;
  197. $dom
  198. } @{$options{mirrors}};
  199. unshift(@{$options{mirrors}}, $options{baseurl});
  200. my $field_name = $options{nossh} ? 'clone_url' : 'ssh_url';
  201. my @repos;
  202. my (%passwords, %clients);
  203. my $cleanup = sub { _cleanup_tokens( \%clients, \%passwords, $options{insecure} ) if %passwords };
  204. foreach my $mirror_url (@{$options{mirrors}}) {
  205. my $server_is_github = _server_is_github($mirror_url);
  206. my ($mirror_domain) = $mirror_url =~ $domainRipper;
  207. my $muser = $options{me};
  208. $muser = $alias_map{$mirror_domain}{$muser} if exists $alias_map{$mirror_domain}{$muser};
  209. my %margs = (
  210. user => $muser,
  211. api_uri => $mirror_url,
  212. );
  213. $margs{token} = $tokens{$mirror_domain} if $tokens{$mirror_domain};
  214. my $mirror = $server_is_github ? Pithub->new(%margs) : Gogs->new(%margs);
  215. # Then it's gogs, and we can just make one.
  216. if (!$margs{token} && !$server_is_github) {
  217. _help(5, "Program must be run interactively to auto-create keys on Gogs installs.") unless IO::Interactive::Tiny::is_interactive();
  218. # Stash the password in case we gotta clean up
  219. $passwords{$mirror_domain} = _prompt("Please type in the password for ".$mirror->user.":");
  220. $tokens{$mirror_domain} = $mirror->get_token(
  221. name => "git-clone-entity",
  222. password => $passwords{$mirror_domain},
  223. insecure => $options{insecure},
  224. );
  225. _help(6, "Could not fetch token from gogs! Check that you supplied the correct username & password.") unless $tokens{$mirror_domain};
  226. $mirror->token($tokens{$mirror_domain});
  227. # Stash for later use by cleanup routines if needed
  228. $clients{$mirror_domain} = $mirror;
  229. }
  230. my @fetched = _fetch_all($mirror, clone($options{users}), clone($options{orgs}), \%alias_map, $field_name);
  231. _help(7, "The provided server ($mirror_url) could not list repos!", $cleanup ) unless @fetched;
  232. # GOGS will list all the repos the user *has access to* not all the ones they own.
  233. @fetched = grep { $_->{owner}{login} eq $_->{user} } @fetched;
  234. push(@repos, @fetched);
  235. }
  236. my ($primary_domain) = $options{baseurl} =~ $domainRipper;
  237. my $transform = sub {
  238. my $repo = shift;
  239. my $reversed = $alias_reverse{$repo->{domain}} // {};
  240. my $aliased = exists $reversed->{$repo->{owner}{login}} ? $reversed->{$repo->{owner}{login}} : $repo->{owner}{login};
  241. my $on_baseurl = $repo->{domain} eq $primary_domain;
  242. my $is_prime = $aliased eq $prime_name;
  243. return {
  244. name => $repo->{name},
  245. clone_uri => $repo->{$field_name},
  246. parent => $repo->{upstream_uri},
  247. private => $repo->{private},
  248. is_primary_domain => $repo->{domain} eq $primary_domain,
  249. domain => $repo->{domain},
  250. upstream => $on_baseurl && $is_prime,
  251. owner => $aliased,
  252. owner_noalias => $repo->{owner}{login},
  253. origin => $repo->{owner}{login} eq $options{me},
  254. api => $clients{$repo->{domain}},
  255. };
  256. };
  257. # get rid of everything that doesn't matter
  258. @repos = map { $transform->($_) } @repos;
  259. my @names = uniq map { $_->{name} } @repos;
  260. my %proper_owner = (
  261. origin => $options{me},
  262. upstream => $prime_name,
  263. );
  264. my %repodata;
  265. foreach my $reponame (@names) {
  266. my @matching = grep { $_->{name} eq $reponame } @repos;
  267. # We don't care about cloning anything which do not intersect with our interests.
  268. # This means we need at least one clone of a repo done by ourself or the prime.
  269. next unless any { $_->{owner} eq $options{me} || $_->{owner} eq $prime_name } @matching;
  270. # There's also no point in making local-org copies of stuff on a mirror (say github)
  271. # Which are forks of other stuff on your users' mirror account (standard workflow).
  272. # Just clone it and call it a day.
  273. my $all_on_mirror = all { $_->{domain} ne $primary_domain && $_->{owner} eq $options{me} } @matching;
  274. my $all_is_forks = all { $_->{parent} } @matching;
  275. if ($all_on_mirror && $all_is_forks) {
  276. # There can only be one at this point.
  277. my $repo = $matching[0];
  278. $repodata{$reponame}{origin}{fetch} = $repo->{clone_uri};
  279. $repodata{$reponame}{origin}{push} //= [];
  280. push(@{$repodata{$reponame}{origin}{push}}, $repo->{clone_uri});
  281. $repodata{$reponame}{upstream}{fetch} = $repo->{parent};
  282. $repodata{$reponame}{upstream}{push} //= [];
  283. push(@{$repodata{$reponame}{upstream}{push}}, $repo->{parent});
  284. next;
  285. }
  286. # We only care about remote names that aren't already origin or upstream.
  287. my @owners = grep { $_ ne $prime_name && $_ ne $options{me} } uniq map { $_->{owner} } @matching;
  288. $repodata{$reponame} = {};
  289. # We then need to build all the remotes based on which is appropriate.
  290. foreach my $remote (qw{origin upstream}, @owners) {
  291. # Pick the right match. We may have something on the baseurl or on one of the mirrors.
  292. my @repos = grep { $_->{$remote} || $_->{owner} eq $remote } @matching;
  293. my ($baseurl_repo) = grep { $_->{domain} eq $primary_domain } @repos;
  294. # Figure out where to make it if we have to.
  295. my $on_baseurl = any { $remote eq $_ } qw{origin upstream};
  296. # Supposing we don't have a match, we have to make it.
  297. if (!$baseurl_repo && $on_baseurl) {
  298. LOG("Missing $reponame on $options{baseurl}, going to create");
  299. my $new_repo = _create_repo( $clients{$primary_domain}, $proper_owner{$remote}, $reponame, $primary_domain );
  300. $baseurl_repo = $transform->($new_repo);
  301. push(@matching, $baseurl_repo);
  302. push(@repos, $baseurl_repo);
  303. }
  304. foreach my $repo (@repos) {
  305. # Prefer the baseurl repo if we have it, otherwise just use what you got.
  306. $repodata{$reponame}{$remote}{fetch} = $baseurl_repo->{clone_uri} // $repo->{clone_uri};
  307. $repodata{$reponame}{$remote}{push} //= [];
  308. push(@{$repodata{$reponame}{$remote}{push}}, $repo->{clone_uri});
  309. }
  310. # Now that we almost certainly have a match, let's setup the mirror URIs as add'l pushes.
  311. if ($on_baseurl) {
  312. LOG("Looking for mirrors to $reponame:$remote");
  313. foreach my $mirror_dom (@mirror_domains) {
  314. my ($mirror) = grep { $_->{domain} eq $mirror_dom && $_->{owner} eq $baseurl_repo->{owner} } @matching;
  315. if (!$mirror) {
  316. # Create mirror.
  317. LOG("Missing mirror on $mirror_dom for $reponame, for $baseurl_repo->{owner} creating...");
  318. my $new_mirror = _create_repo( $clients{$mirror_dom}, $alias_map{$mirror_dom}{$baseurl_repo->{owner}}, $reponame, $mirror_dom );
  319. $mirror = $transform->($new_mirror);
  320. push(@matching, $mirror);
  321. }
  322. # Then add it.
  323. push(@{$repodata{$reponame}{$remote}{push}}, $mirror->{clone_uri});
  324. }
  325. }
  326. }
  327. }
  328. $cleanup->();
  329. print Dumper(\%repodata);
  330. die;
  331. _clone_repos(%repodata);
  332. # Clean up
  333. $cleanup->();
  334. return 0;
  335. }
  336. sub _create_repo {
  337. my ($api, $user, $repo, $domain) =@_;
  338. # Double check if the repo actually exists, and just return that (TOCTOU)
  339. #my $content = _fetch_repo($api, $user, { name => $repo }, 1);
  340. LOG("Creating $repo on $domain");
  341. return { name => $repo, 'ssh_url' => "$user\@$domain:$repo TODO", owner => { login => $user }, domain => $domain };
  342. #my $result = $api->repos->create( data => { name => $repo } );
  343. #TODO handle errors
  344. #my $new_repo = $result->content();
  345. #TODO fortify the new repo with the needed infos along the lines of L335
  346. #TODO set upstream fork if applicable
  347. }
  348. sub _clone_repos {
  349. my (%repodata) = @_;
  350. foreach my $to_clone (keys(%repodata)) {
  351. #XXX testing removme
  352. next unless $to_clone eq 'perl-Gogs';
  353. my $r = $repodata{$to_clone};
  354. # Don't clone it if it is already present.
  355. if (!-d $to_clone) {
  356. LOG("Cloning $to_clone...");
  357. my $res = Git::command_oneline([ 'clone', $r->{origin}{fetch} ]);
  358. }
  359. LOG("Entering $to_clone...");
  360. my $repo = Git->repository(Directory => $to_clone);
  361. # Figure out what the remotes look like
  362. my $res = $repo->command(qw{remote -v});
  363. my %remotes = _parse_remotes($res);
  364. # Make sure all the remotes are setup correctly.
  365. foreach my $rname (keys(%$r)) {
  366. my $remote = $r->{$rname};
  367. LOG("Setting up remote $rname...");
  368. $repo->command(qw{remote rm}, $rname);
  369. $repo->command(qw{remote add}, $rname, $remote->{fetch});
  370. $repo->command(qw{fetch}, $rname);
  371. foreach my $push_uri (@{$remote->{push}}) {
  372. next if $push_uri eq $remote->{fetch};
  373. $res = $repo->command(qw{remote set-url --add --push}, $rname, $push_uri);
  374. }
  375. }
  376. # TODO figure out which mirrors are missing, and add them if needed (consider privacy)
  377. # 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.
  378. }
  379. }
  380. sub _fetch_repo {
  381. my ($mirror, $muser, $repo, $nonfatal) = @_;
  382. my $details = $mirror->repos->get( user => $muser, repo => $repo->{name});
  383. if (!$details || !$details->response->is_success()) {
  384. return if $nonfatal;
  385. _help(9, "Could not fetch repository details for $repo->{name}") unless $details && $details->response->is_success();
  386. }
  387. return $details->content();
  388. }
  389. sub _fetch_upstream_uri {
  390. my ($mirror, $field_name, $muser, $repo) = @_;
  391. my $upstream_uri;
  392. if ($repo->{fork}) {
  393. LOG("Looking up what $repo->{name} was forked from...");
  394. my $content = _fetch_repo($mirror, $muser, $repo);
  395. $upstream_uri = $content->{parent}{$field_name};
  396. _help(10, "Could not discern upstream URI for forked repo $repo->{name}!") unless $upstream_uri;
  397. }
  398. return $upstream_uri;
  399. }
  400. sub _parse_remotes {
  401. my ($raw) = shift;
  402. my %parsed;
  403. foreach my $line (split(/\n/, $raw)) {
  404. my ($name, $uri, $type) = $line =~ m/^(.+)\s+(.+)\s+\((.+)\)$/;
  405. if ($type eq 'fetch') {
  406. $parsed{$name}{$type} = $uri;
  407. } else {
  408. $parsed{$name}{$type} //= [];
  409. push(@{$parsed{$name}{$type}}, $uri);
  410. }
  411. }
  412. return %parsed;
  413. }
  414. sub _cleanup_tokens {
  415. my ( $apis, $passwords, $insecure ) = @_;
  416. foreach my $domain (keys(%$apis)) {
  417. my $api = $apis->{$domain};
  418. my $result = $api->delete_token( sha1 => $api->token, password => $passwords->{$domain}, insecure => $insecure );
  419. die "Could not clean up token" unless $result && $result->response->is_success;
  420. }
  421. }
  422. sub _prompt {
  423. my ( $prompt ) = @_;
  424. $prompt ||= "";
  425. my $input = "";
  426. print $prompt;
  427. # We are readin a password
  428. Term::ReadKey::ReadMode('noecho');
  429. {
  430. local $SIG{'INT'} = sub { Term::ReadKey::ReadMode(0); exit 130; };
  431. $input = <STDIN>;
  432. chomp($input) if $input;
  433. }
  434. Term::ReadKey::ReadMode(0);
  435. print "\n";
  436. return $input;
  437. }
  438. sub _fetch_all {
  439. my ($api, $users, $orgs, $alias_map, $field_name) = @_;
  440. my ($domain) = $api->api_uri =~ $domainRipper;
  441. # TODO detect which repo among forks is the "primary" (if one of them is not a fork, use it)
  442. my @repos;
  443. foreach my $user (@$users) {
  444. LOG("Fetching repos for $user...");
  445. $user = $alias_map->{$domain}{$user} if exists $alias_map->{$domain}{$user};
  446. my $result = $api->repos->list( user => $user );
  447. my @fetched = _array_content($result);
  448. @fetched = _augment_repos($api, $field_name, $user, $domain, @fetched);
  449. push(@repos, @fetched);
  450. }
  451. foreach my $org (@$orgs) {
  452. LOG("Fetching repos for $org...");
  453. $org = $alias_map->{$domain}{$org} if exists $alias_map->{$domain}{$org};
  454. my $result = $api->repos->list( org => $org );
  455. my @fetched = _array_content($result);
  456. @fetched = _augment_repos($api, $field_name, $org, $domain, @fetched);
  457. push(@repos, @fetched);
  458. }
  459. return @repos;
  460. }
  461. sub _array_content {
  462. my ($result) = @_;
  463. return () unless $result && $result->response->is_success;
  464. return @{$result->content()} if ref $result->content() eq 'ARRAY';
  465. return ();
  466. }
  467. sub _augment_repos {
  468. my ($mirror, $field_name, $muser, $domain, @fetched) = @_;
  469. @fetched = map {
  470. my $subj = $_;
  471. $subj->{domain} = $domain;
  472. $subj->{upstream_uri} = _fetch_upstream_uri($mirror, $field_name, $muser, $subj);
  473. $subj->{user} = $muser;
  474. $subj
  475. } @fetched;
  476. return @fetched;
  477. }
  478. sub _server_is_github {
  479. my ($uri) = @_;
  480. LOG("Figuring out what kind of server $uri is...");
  481. my $ua = HTTP::Tiny->new();
  482. my $res = $ua->get($uri);
  483. # GOGS will 404 it's api baseurl, github will not
  484. return $res->{success};
  485. }
  486. exit main(@ARGV) unless caller;
  487. 1;