Group
Extension

App-cpangitify/lib/App/cpangitify.pm

package App::cpangitify;

use strict;
use warnings;
use autodie qw( :system );
use 5.020;
use experimental qw( signatures );
use Getopt::Long qw( GetOptions );
use Pod::Usage qw( pod2usage );
use Path::Class qw( file dir );
use Git::Wrapper;
use File::Temp qw( tempdir );
use File::chdir;
use JSON::PP qw( decode_json );
use URI;
use PerlX::Maybe qw( maybe );
use File::Copy::Recursive qw( rcopy );
use File::Basename qw( basename );
use Archive::Libarchive::Extract;
use CPAN::ReleaseHistory;
use HTTP::Tiny;

# ABSTRACT: Convert cpan distribution from BackPAN to a git repository
our $VERSION = '0.20'; # VERSION


our $ua  = HTTP::Tiny->new( verify_SSL => 1 );
our $opt_metacpan_url;

sub _rm_rf ($file)
{
  if($file->is_dir && ! -l $file)
  {
    _rm_rf($_) for $file->children;
  }

  $file->remove || die "unable to delete $file";
}

our $_run_cb = sub {};
our $original_run = \&Git::Wrapper::RUN;
our $trace = 0;

sub _run_wrapper ($self, @command)
{
  my @display;
  foreach my $arg (@command)
  {
    if(ref($arg) eq 'HASH')
    {
      foreach my $k (keys %$arg)
      {
        my $v = $arg->{$k};
        push @display, "--$k";
        push @display, $v =~ /\s/ ? "'$v'" : $v
          if $v ne '1'; # yes there is a weird exception for this :P
      }
    }
    else
    {
      push @display, $arg;
    }
  }
  $_run_cb->($self, @display);
  say "+ git @display" if $trace;
  $original_run->($self, @command);
}

sub author ($cpanid)
{
  state $cache = {};

  unless(defined $cache->{$cpanid})
  {
    my $uri = URI->new($opt_metacpan_url . "v1/author/" . $cpanid);
    my $res = $ua->get($uri);
    unless($res->{success})
    {
      say "error fetching $uri";
      say $res->{reason};
      return 2;
    }
    $cache->{$cpanid} = decode_json($res->{content})
  }

  my $email = $cache->{$cpanid}->{email};
  $email = $email->[0] if ref($email) eq 'ARRAY';
  sprintf "%s <%s>", $cache->{$cpanid}->{name}, $email;
}

sub main ($, @args)
{
  local @ARGV = @args;
  no warnings 'redefine';
  local *Git::Wrapper::RUN = \&_run_wrapper;
  use warnings;

  my %skip;
  my $opt_backpan_index_url;
  my $opt_backpan_url = "http://backpan.perl.org/authors/id";
  $opt_metacpan_url   = "http://fastapi.metacpan.org/";
  my $opt_trace = 0;
  my $opt_output;
  my $opt_resume;
  my $opt_branch = 'main';

  GetOptions(
    'backpan_index_url=s' => \$opt_backpan_index_url,
    'backpan_url=s'       => \$opt_backpan_url,
    'metacpan_url=s'      => \$opt_metacpan_url,
    'trace'               => \$opt_trace,
    'skip=s'              => sub ($version) { $skip{$_} = 1 for split /,/, $version },
    'resume'              => \$opt_resume,
    'output|o=s'          => \$opt_output,
    'help|h'              => sub { pod2usage({ -verbose => 2}) },
    'branch|b=s'          => \$opt_branch,
    'version'             => sub {
      say 'cpangitify version ', ($App::cpangitify::VERSION // 'dev');
      exit 1;
    },
  ) || pod2usage(1);

  local $trace = $opt_trace;

  my @names = @ARGV;
  s/::/-/g for @names;
  my %names = map { $_ => 1 } @names;
  my $name = $names[0];

  pod2usage(1) unless $name;

  my $dest = $opt_output ? dir($opt_output)->absolute : dir()->absolute->subdir($name);

  if(-e $dest && ! $opt_resume)
  {
    say "already exists: $dest";
    say "you may be able to update with the --resume option";
    say "but any local changes to your repository will be overwritten by upstream";
    return 2;
  }

  say "creating/updating index...";
  my $history = CPAN::ReleaseHistory->new(
    maybe url => $opt_backpan_index_url
  )->release_iterator;

  say "searching...";
  my @rel;
  while(my $release = $history->next_release)
  {
    next unless defined $release->distinfo->dist;
    next unless $names{$release->distinfo->dist};
    push @rel, $release;
  }

  if($@ || @rel == 0)
  {
    say "no releases found for $name";
    return 2;
  }

  say "mkdir $dest";
  $dest->mkpath(0,0700);

  my $git = Git::Wrapper->new($dest->stringify);

  if($opt_resume)
  {
    if($git->status->is_dirty)
    {
      die "the appear to be uncommited changes";
    }
    $skip{$_} = 1 for $git->tag;
  }
  else
  {
    $git->init;
    $git->checkout( -b => $opt_branch );
  }

  foreach my $rel (@rel)
  {
    my $path    = $rel->path;
    my $version = $rel->distinfo->version;
    my $time    = $rel->timestamp;
    my $cpanid  = $rel->distinfo->cpanid;

    say "$path [ $version ]";

    if($skip{$version})
    {
      say "skipping ...";
      next;
    }

    my $tmp = dir( tempdir( CLEANUP => 1 ) );

    local $CWD = $tmp->stringify;

    my $uri = URI->new(join('/', $opt_backpan_url, $path));
    say "fetch ... $uri";
    my $res = $ua->get($uri);
    unless($res->{success})
    {
      say "error fetching $uri";
      say $res->{reason};
      return 2;
    }

    say "unpack... @{[ basename $uri->path ]}";
    my $extract = Archive::Libarchive::Extract->new(
      memory => \$res->{content},
      entry => sub ($e) {
        say "- extract @{[ $e->pathname ]}" if $trace;
        1;
      },
    );
    $extract->extract( to => $CWD );

    my $source = do {
      my @children = map { $_->absolute } dir()->children;
      if(@children != 1)
      {
        say "archive doesn't contain exactly one child: @children";
      }

      $CWD = $children[0]->stringify;
      $children[0];
    };

    say "merge...";

    foreach my $child ($dest->children)
    {
      next if $child->basename eq '.git';
      _rm_rf($child);
    }

    foreach my $child ($source->children)
    {
      next if $child->basename eq '.git';
      if(-d  $child)
      {
        rcopy($child, $dest->subdir($child->basename)) || die "unable to copy $child $!";
      }
      else
      {
        rcopy($child, $dest->file($child->basename)) || die "unable to copy $child $!";
      }
    }

    say "commit and tag...";
    $git->add('.');
    $git->add('-u');
    $git->commit({
      message       => "version $version",
      date          => "$time +0000",
      author        => author($cpanid),
      'allow-empty' => 1,
    });
    eval { $git->tag($version) };
    warn $@ if $@;
  }

  return 0;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

App::cpangitify - Convert cpan distribution from BackPAN to a git repository

=head1 VERSION

version 0.20

=head1 DESCRIPTION

This is the module for the L<cpangitify> script.  See L<cpangitify> for details.

=head1 SEE ALSO

L<cpangitify>

=head1 AUTHOR

Author: Graham Ollis E<lt>plicease@cpan.orgE<gt>

Contributors:

Mohammad S Anwar (MANWAR)

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2013-2022 by Graham Ollis.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.