AnyEvent-Git-Wrapper

 view release on metacpan or  search on metacpan

lib/AnyEvent/Git/Wrapper.pm  view on Meta::CPAN

package AnyEvent::Git::Wrapper;

use strict;
use warnings;
use Carp qw( croak );
use base qw( Git::Wrapper );
use File::pushd;
use AnyEvent;
use AnyEvent::Open3::Simple;
use Git::Wrapper::Exception;
use Git::Wrapper::Statuses;
use Git::Wrapper::Log;
use Scalar::Util qw( blessed );

# ABSTRACT: Wrap git command-line interface without blocking
our $VERSION = '0.10'; # VERSION


sub new
{
  my $class = shift;
  
  my $args;
  if(scalar @_ == 1)
  {
    my $arg = shift;
    if(ref $arg eq 'HASH') { $args = $arg }
    elsif(blessed $arg)    { $args = { dir => "$arg" } }
    elsif(! ref $arg)      { $args = { dir => $arg } }
    else { die "Singlearg must be hashref, scalar or stringify-able object" }
  }
  else
  {
    my($dir, %opts) = @_;
    $dir = "$dir" if blessed $dir;
    $args = { dir => $dir, %opts };
  }
  my $cache_version = delete $args->{cache_version};
  my $self = $class->SUPER::new($args);
  $self->{ae_cache_version} = $cache_version;
  $self;
}


sub RUN
{
  my($self) = shift;
  my $cv;
  if(ref($_[-1]) eq 'CODE')
  {
    $cv = AE::cv;
    $cv->cb(pop);
  }
  elsif(eval { $_[-1]->isa('AnyEvent::CondVar') })
  {
    $cv = pop;
  }
  else
  {
    return $self->SUPER::RUN(@_);
  }

  my $cmd = shift;

  my $customize;
  $customize = pop if ref($_[-1]) eq 'CODE';
  
  my ($parts, $in) = Git::Wrapper::_parse_args( $cmd, @_ );
  my @out;
  my @err;

  my $ipc = AnyEvent::Open3::Simple->new(
    on_stdout => \@out,
    on_stderr => \@err,
    on_error  => sub {
      #my($error) = @_;
      $cv->croak(
        Git::Wrapper::Exception->new(
          output => \@out,
          error  => \@err,
          status => -1,
        )
      );
    },
    on_exit   => sub {
      my(undef, $exit, $signal) = @_;
      
      # borrowed from superclass, see comment there
      my $stupid_status = $cmd eq 'status' && @out && ! @err;
      
      if(($exit || $signal) && ! $stupid_status)
      {
        $cv->croak(
          Git::Wrapper::Exception->new(
            output => \@out,
            error  => \@err,
            status => $exit,
          )
        );
      }
      else
      {
        $self->{err} = \@err;
        $self->{out} = \@out;
        $cv->send(\@out, \@err);
      }
    },
    $customize ? $customize->() : ()
  );
  
  do {
    my $d = pushd $self->dir unless $cmd eq 'clone';
    
    my @cmd = ( $self->git, @$parts );
    
    local $ENV{GIT_EDITOR} = $^O eq 'MSWin32' ? 'cmd /c "exit 2"' : '';
    $ipc->run(@cmd, \$in);
    
    undef $d;
  };
  
  $cv;
}


my %STATUS_CONFLICTS = map { $_ => 1 } qw<DD AU UD UA DU AA UU>;

sub status
{
  my($self) = shift;
  my $cv;
  if(ref($_[-1]) eq 'CODE')
  {
    $cv = AE::cv;
    $cv->cb(pop);
  }
  elsif(eval { $_[-1]->isa('AnyEvent::CondVar') })
  {
    $cv = pop;
  }
  else
  {
    return $self->SUPER::status(@_);
  }

  my $opt = ref $_[0] eq 'HASH' ? shift : {};
  $opt->{porcelain} = 1;

  $self->RUN('status' => $opt, @_, sub {
    my $out = shift->recv;
    my $stat = Git::Wrapper::Statuses->new;

    for(@$out)
    {
      my ($x, $y, $from, $to) = $_ =~ /\A(.)(.) (.*?)(?: -> (.*))?\z/;
      if ($STATUS_CONFLICTS{"$x$y"})
      {
        $stat->add('conflict', "$x$y", $from, $to);
      }
      elsif ($x eq '?' && $y eq '?')
      {
        $stat->add('unknown', '?', $from, $to);
      }
      else
      {
        $stat->add('changed', $y, $from, $to)
          if $y ne ' ';
        $stat->add('indexed', $x, $from, $to)
          if $x ne ' ';
      }
    }
    
    $cv->send($stat);
  });
  
  $cv;
}


sub log
{
  my($self) = shift;
  my $cv;
  if(ref($_[-1]) eq 'CODE')
  {
    $cv = AE::cv;
    $cv->cb(pop);
  }
  elsif(eval { $_[-1]->isa('AnyEvent::CondVar') })
  {
    $cv = pop;
  }
  else
  {
    return $self->SUPER::log(@_);
  }
  
  my $cb;
  if(ref($_[-1]) eq 'CODE')
  {
    $cb = pop;
  }

  my $opt = ref $_[0] eq 'HASH' ? shift : {};
  $opt->{no_color}         = 1;
  $opt->{pretty}           = 'medium';
  $opt->{no_abbrev_commit} = 1
    if $self->supports_log_no_abbrev_commit;
  
  my $raw = defined $opt->{raw} && $opt->{raw};

  my $out = [];
  my @logs;
  
  my $process_commit = sub {
    if(my $line = shift @$out)
    {
      unless($line =~ /^commit (\S+)/)
      {
        $cv->croak("unhandled: $line");
        return;
      }
      
      my $current = Git::Wrapper::Log->new($1);
      
      $line = shift @$out;  # next line
      
      while($line =~ /^(\S+):\s+(.+)$/)
      {
        $current->attr->{lc $1} = $2;
        $line = shift @$out; # next line
      }
      
      if($line)
      {
        $cv->croak("no blank line separating head from message");
        return;
      }
      
      my($initial_indent) = $out->[0] =~ /^(\s*)/ if @$out;
      
      my $message = '';
      while(@$out and $out->[0] !~ /^commit (\S+)/ and length($line = shift @$out))
      {
        $line =~ s/^$initial_indent//; # strip just the indenting added by git
        $message .= "$line\n";
      }
      
      $current->message($message);
      
      if($raw)
      {
        my @modifications;
        while(@$out and $out->[0] =~ m/^\:(\d{6}) (\d{6}) (\w{7})\.\.\. (\w{7})\.\.\. (\w{1})\t(.*)$/)
        {
          push @modifications, Git::Wrapper::File::RawModification->new($6,$5,$1,$2,$3,$4);
          shift @$out;
        }
        $current->modifications(@modifications) if @modifications;
      }
      
      if($cb)
      { $cb->($current) }
      else
      { push @logs, $current }
    }
  };
  
  my $on_stdout = sub {
    my $line = pop;
    push @$out, $line;
    $process_commit->() if $line =~ /^commit (\S+)/ && @$out > 1;
  };
  
  $self->RUN(log => $opt, @_, sub { on_stdout => $on_stdout }, sub {
    eval { shift->recv };
    $cv->croak($@) if $@;
    
    while($out->[0]) {
      $process_commit->();
    }
    
    $cv->send(@logs);
  });
  
  $cv;
}


sub version
{
  my($self) = @_;
  my $cv;
  if(ref($_[-1]) eq 'CODE')
  {
    $cv = AE::cv;
    $cv->cb(pop);
  }
  elsif(eval { $_[-1]->isa('AnyEvent::CondVar') })
  {
    $cv = pop;
  }
  else
  {
    if($self->{ae_cache_version} && $self->{ae_version})
    { return $self->{ae_version} }
    $self->{ae_version} = $self->SUPER::version(@_);
    return $self->{ae_version};
  }
  
  if($self->{ae_cache_version} && $self->{ae_version})
  {
    $cv->send($self->{ae_version});
  }
  else
  {
    $self->RUN('version', sub {
      my $out = eval { shift->recv };
      if($@)
      {
        $cv->croak($@);
      }
      else
      {
        $self->{ae_version} = $out->[0];
        $self->{ae_version} =~ s/^git version //;
        $cv->send($self->{ae_version});
      }
    });
  }
  
  $cv;
}


1;

__END__

=pod

=encoding UTF-8

=head1 NAME

AnyEvent::Git::Wrapper - Wrap git command-line interface without blocking

=head1 VERSION

version 0.10

=head1 SYNOPSIS

 use AnyEvent::Git::Wrapper;
 
 # add all files and make a commit...
 my $git = AnyEvent::Git::Wrapper->new($dir);
 $git->add('.', sub {
   $git->commit({ message => 'initial commit' }, sub {
     say "made initial commit";
   });
 });

=head1 DESCRIPTION

B<DEPRECATED>: May go away at some point.



( run in 1.153 second using v1.01-cache-2.11-cpan-8f98c5d2c55 )