Group
Extension

App-GitHub/lib/App/GitHub.pm

package App::GitHub;

use strict;
use warnings;

# ABSTRACT: GitHub Command Tools

use Carp;
use Moose;
use Net::GitHub;
use Term::ReadKey;
use Term::ReadLine;
use JSON::XS;
use IPC::Cmd qw/can_run/;

our $VERSION = '1.0.1';

has 'term' => (
    is       => 'rw',
    required => 1,
    default  => sub { Term::ReadLine->new('Perl-App-GitHub') }
);
has 'prompt' => (
    is       => 'rw',
    required => 1,
    default  => sub { 'github> ' }
);

has 'out_fh' => (
    is       => 'rw',
    required => 1,
    lazy     => 1,
    default  => sub {
        shift->term->OUT || \*STDOUT;
    }
);

has 'repo_regexp' => (
    is       => 'ro',
    required => 1,
    isa      => 'RegexpRef',
    default  => sub { qr/^([\-\w]+)[\/\\\s]([\-\w]+)$/ }
);

# For non-interactive mode
has 'silent' => (
    is       => 'rw',
    required => 1,
    default  => 0,
);

sub print_err {
    shift->print( @_, 1 );
}

sub print {
    my ( $self, $message, $error ) = @_;
    return 1 if $self->silent and not $error;

    my $fh;
    local $@;
    my $rows         = ( GetTerminalSize( $self->out_fh ) )[1];
    my $message_rows = $message =~ tr/\n/\n/;
    my $pager_use    = 0;

    # let less exit if one screen
    no warnings 'uninitialized';
    local $ENV{LESS} ||= "";
    $ENV{LESS} .= " -F";
    use warnings;

    if ( $@ or $message_rows < $rows ) {
        chomp $message;
        $message .= "\n";

        $fh = $self->out_fh;
    }
    else {
        eval {
            open $fh, '|-', $self->_get_pager
              or croak "unable to open more: $!";
        }
          or $fh = $self->out_fh;
        $pager_use = 1;
    }

    no warnings 'uninitialized';
    print $fh "$message";
    print $fh "\n" if $self->term->ReadLine =~ /Gnu/;
    close($fh) if $pager_use;
}

sub _get_pager {
    my $pager =
         $ENV{PAGER}
      || can_run("less")
      || can_run("more")
      || croak "no pager found";
}

sub read {
    my ( $self, $prompt ) = @_;
    $prompt ||= $self->prompt;
    return $self->term->readline($prompt);
}

has 'github' => (
    is  => 'rw',
    isa => 'Net::GitHub',
);

has '_data' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );

my $dispatch = {
    'exit' => sub { exit; },
    'quit' => sub { exit; },
    'q'    => sub { exit; },
    '?'    => \&help,
    'h'    => \&help,

    # Common
    repo    => \&set_repo,
    login   => \&set_login,
    loadcfg => \&set_loadcfg,

    # Repo
    'r.show' => sub { shift->run_basic_repo_cmd( 'repos', 'get', shift ); },
    'r.list'  => \&repo_list,
    'r.watch' => sub { shift->run_basic_repo_cmd( 'repos', 'watch', shift ); },
    'r.unwatch' =>
      sub { shift->run_basic_repo_cmd( 'repos', 'unwatch', shift ); },
    'r.fork' =>
      sub { shift->run_basic_repo_cmd( 'repos', 'create_fork', shift ); },
    'r.create'      => \&repo_create,
    'r.set_private' => sub { shift->repo_update( private => \1, shift ); },
    'r.set_public'  => sub { shift->repo_update( private => \0, shift ); },

    # XXX? TODO, deploy_keys collaborators
    'r.commit' =>
      sub { shift->run_github_with_repo( 'git_data', 'commit', shift ); },

    # Issues
    'i.list' => sub {
        my ( $self, $type ) = @_;
        $type ||= 'open';
        $self->run_github_with_repo( 'issue', 'repos_issues',
            { state => $type } );
    },
    'i.view' => sub { shift->run_github_with_repo( 'issue', 'issue', shift ); },
    'i.open' => sub { shift->issue_open_or_edit('open') },
    'i.edit'  => sub { shift->issue_open_or_edit( 'edit', @_ ) },
    'i.close' => sub {
        shift->run_github_with_repo( 'issue', 'update_issue', shift,
            { state => 'closed' } );
    },
    'i.reopen' => sub {
        shift->run_github_with_repo( 'issue', 'update_issue', shift,
            { state => 'open' } );
    },
    'i.label'   => \&issue_label,
    'i.comment' => \&issue_comment,

    # User
    'u.show'   => sub { shift->run_github( 'user', 'show', shift ); },
    'u.update' => \&user_update,
    'u.followers' => sub { shift->run_github( 'user', 'followers' ); },
    'u.following' => sub { shift->run_github( 'user', 'following' ); },
    'u.follow'    => sub { shift->run_github( 'user', 'follow', shift ); },
    'u.unfollow'  => sub { shift->run_github( 'user', 'unfollow', shift ); },
    'u.pub_keys'     => sub { shift->user_pub_keys('show'); },
    'u.pub_keys.add' => sub { shift->user_pub_keys( 'add', @_ ); },
    'u.pub_keys.del' => sub { shift->user_pub_keys( 'del', @_ ); },

    # Object
    'o.tree' =>
      sub { shift->run_github_with_repo( 'git_data', 'tree', shift ); },
    'o.trees' =>
      sub { shift->run_github_with_repo( 'git_data', 'tree', shift ); },
    'o.blob' =>
      sub { shift->run_github_with_repo( 'git_data', 'blob', shift ); },
};

sub run {
    my $self = shift;

    $self->print(<<START);
Welcome to GitHub Command Tools! (Ver: $VERSION)
Type '?' or 'h' for help.
START

    $self->set_loadcfg(1);
    while ( defined( my $command = $self->read ) ) {

        $command =~ s/(^\s+|\s+$)//g;
        next unless length($command);

        # check dispatch
        if ( exists $dispatch->{$command} ) {
            $dispatch->{$command}->($self);
        }
        else {

            # split the command out
            ( $command, my $args ) = split( /\s+/, $command, 2 );
            if ( $command and exists $dispatch->{$command} ) {
                $dispatch->{$command}->( $self, $args );
            }
            else {
                $self->print("Unknown command, type '?' or 'h' for help");
                next unless $command;
            }
        }

        $self->term->addhistory($command) if $command =~ /\S/;
    }
}

sub help {
    my $self = shift;
    $self->print(<<HELP);
 command   argument          description
 repo      :user :repo       set owner/repo, eg: 'fayland perl-app-github'
 login     :login :pass      authenticated as :login
 loadcfg                     authed by git config --global github.user|pass
 ?,h                         help
 q,exit,quit                 exit

Repos
 r.show                      more in-depth information for the :repo
 r.list                      list out all the repositories for the :user
 r.watch                     watch repositories (auth required)
 r.unwatch                   unwatch repositories (auth required)
 r.fork                      fork a repository (auth required)
 r.create                    create a new repository (auth required)
 r.set_private               set a public repo private (auth required)
 r.set_public                set a private repo public (auth required)
 r.commit    :sha1           show a specific commit

Issues
 i.list    open|closed       see a list of issues for a project
 i.view    :number           get data on an individual issue by number
 i.open                      open a new issue (auth required)
 i.close   :number           close an issue (auth required)
 i.reopen  :number           reopen an issue (auth required)
 i.edit    :number           edit an issue (auth required)
 i.comment :number
 i.label   add|del :num :label
                             add/remove a label (auth required)

Users
 u.show                      get extended information on user
 u.update                    update your users info (auth required)
 u.followers
 u.following
 u.follow  :user             follow :user (auth required)
 u.unfollow :user            unfollow :user (auth required)
 u.pub_keys                  Public Key Management (auth required)
 u.pub_keys.add
 u.pub_keys.del :number

Objects
 o.tree    :tree_sha1        get the contents of a tree by tree sha
 o.trees   :tree_sha1        get the contents of a tree by tree sha and recursively descend down the tree
 o.blob    :sha1             get the data of a blob (tree, file or commits)

Others
 r.show    :user :repo       more in-depth information for a repository
 r.list    :user             list out all the repositories for a user
 u.show    :user             get extended information on :user
HELP
}

sub set_repo {
    my ( $self, $repo ) = @_;

    # validate
    unless ( $repo =~ $self->repo_regexp ) {
        $self->print("Wrong repo args ($repo), eg 'fayland perl-app-github'");
        return;
    }
    my ( $owner, $name ) = ( $repo =~ $self->repo_regexp );
    $self->{_data}->{owner} = $owner;
    $self->{_data}->{repo}  = $name;

    # when call 'login' before 'repo'
    my @logins = ( $self->{_data}->{login} and $self->{_data}->{pass} )
      ? (
        login => $self->{_data}->{login},
        pass  => $self->{_data}->{pass}
      )
      : ();

    $self->{github} = Net::GitHub->new(
        owner   => $owner,
        repo    => $name,
        version => 3,
        pass    => $self->{_data}->{pass},
        @logins,
    );
    $self->{prompt} = "$owner/$name> ";
}

sub set_login {
    my ( $self, $login ) = @_;

    ( $login, my $pass ) = split( /\s+/, $login, 2 );
    unless ( $login and $pass ) {
        $self->print("Wrong login args ($login $pass), eg fayland password");
        return;
    }

    $self->_do_login( $login, $pass );
}

sub set_loadcfg {
    my ( $self, $ign ) = @_;

    my $login = `git config --global github.user`;
    my $pass  = `git config --global github.pass`;
    chomp($login);
    chomp($pass);
    unless ( ( $login and $pass ) or $ign ) {
        $self->print("run git config --global github.user|pass fails");
        return;
    }

    $self->_do_login( $login, $pass ) if $login and $pass;
}

sub _do_login {
    my ( $self, $login, $pass ) = @_;

    # save for set_repo
    $self->{_data}->{login} = $login;
    $self->{_data}->{pass}  = $pass;

    if ( $self->{_data}->{repo} ) {
        $self->{github} = Net::GitHub->new(
            version => 3,
            owner   => $self->{_data}->{owner},
            repo    => $self->{_data}->{repo},
            login   => $self->{_data}->{login},
            pass    => $self->{_data}->{pass}
        );
    }
    else {

        # Create a Net::GitHub object with the owner set to the logged in user
        # Super convenient if you don't want to set a user first
        $self->{github} = Net::GitHub->new(
            version => 3,
            login   => $self->{_data}->{login},
            pass    => $self->{_data}->{pass},
            owner   => $self->{_data}->{login}
        );
    }
}

sub run_github {
    my ( $self, $c1, $c2 ) = @_;

    unless ( $self->github ) {
        croak "not auth" if $self->silent;
        $self->print(
            q~not enough information. try calling login :user :pass or loadcfg~
        );
        return;
    }

    my @args = splice( @_, 3, scalar @_ - 3 );
    eval {
        my $result = $self->github->$c1->$c2(@args);

        # o.blob return plain text
        if ( ref $result ) {
            $result = JSON::XS->new->utf8->pretty->encode($result);
        }
        $self->print($result);
    };

    if ($@) {

        # custom error
        if ( $@ =~ /login and pass are required/ ) {
            croak "not auth" if $self->silent;
            $self->print(
qq~authentication required.\ntry 'login :owner :pass' or 'loadcfg' first\n~
            );
        }
        else {
            croak $@ if $self->silent;
            $self->print_err($@);
        }
    }
}

sub run_github_with_repo {
    my ($self) = shift;

    unless ( $self->{_data}->{repo} ) {
        $self->print(q~no repo specified. try calling repo :owner :repo~);
        return;
    }

    $self->run_github(
        shift, shift,
        $self->{_data}->{owner},
        $self->{_data}->{repo}, @_
    );
}

sub run_basic_repo_cmd {
    my ( $self, $obj, $meth, $args ) = @_;

    if ( $args and $args =~ $self->repo_regexp ) {
        $self->run_github( $obj, $meth, $1, $2 );
    }
    else {
        $self->run_github_with_repo( $obj, $meth );
    }
}

################## Repos
sub repo_list {
    my ( $self, $args ) = @_;
    if ( $args and $args =~ /^[\w\-]+$/ ) {
        $self->run_github( 'repos', 'list', $args );
    }
    else {
        $self->run_github( 'repos', 'list' );
    }
}

sub repo_create {
    my ($self) = shift;

    my %data;
    unless (@_) {
        foreach my $col ( 'name', 'description', 'homepage' ) {
            my $data = $self->read( ucfirst($col) . ': ' );
            $data{$col} = $data;
        }
    }
    else {
        ( $data{name}, $data{description}, $data{homepage} ) = @_;
    }

    unless ( length( $data{name} ) ) {
        $self->print('create repo failed. name is required');
        return;
    }

    $self->run_github( 'repos', 'create', \%data );
}

sub repo_update {
    my ( $self, $param, $value, $args ) = @_;

    if ( $args and $args =~ $self->repo_regexp ) {
        $self->run_github( 'repos', 'update', $1, $2,
            { $param => $value, name => $2 } );
    }
    else {
        unless ( $self->{_data}->{repo} ) {
            $self->print(q~no repo specified. try calling repo :owner :repo~);
            return;
        }

        $self->run_github_with_repo( 'repos', 'update',
            { $param => $value, name => $self->{_data}->{repo} } );
    }
}

# Issues
sub issue_open_or_edit {
    my ( $self, $type, $number ) = @_;

    if ( $type eq 'edit' and $number !~ /^\d+$/ ) {
        $self->print('unknown argument. i.edit :number');
        return;
    }

    my $title = $self->read('Title: ');
    my $body  = $self->read('Body (use EOF to submit, use QUIT to cancel): ');
    while ( my $data = $self->read('> ') ) {
        last   if ( $data eq 'EOF' );
        return if ( $data eq 'QUIT' );
        $body .= "\n" . $data;
    }

    if ( $type eq 'edit' ) {
        $self->run_github_with_repo( 'issue', 'update_issue', $number,
            { title => $title, body => $body } );
    }
    else {
        $self->run_github_with_repo( 'issue', 'create_issue',
            { title => $title, body => $body } );
    }
}

sub issue_label {
    my ( $self, $args ) = @_;

    no warnings 'uninitialized';
    my ( $type, $number, $label ) = split( /\s+/, $args, 3 );
    if ( $type eq 'add' ) {
        $self->run_github_with_repo( 'issue', 'create_issue_label', $number,
            [$label] );
    }
    elsif ( $type eq 'del' ) {
        $self->run_github_with_repo( 'issue', 'delete_issue_label', $number,
            $label );
    }
    else {
        $self->print('unknown argument. i.label add|del :number :label');
    }
}

sub issue_comment {
    my ( $self, $number ) = @_;

    if ( $number !~ /^\d+$/ ) {
        $self->print('unknown argument. i.comment :number');
        return;
    }

    my $body = $self->read('Comment (use EOF to submit, use QUIT to cancel): ');
    while ( my $data = $self->read('> ') ) {
        last   if ( $data eq 'EOF' );
        return if ( $data eq 'QUIT' );
        $body .= "\n" . $data;
    }

    $self->run_github_with_repo( 'issue', 'create_comment', $number,
        { body => $body } );
}

################## Users
sub user_update {
    my ( $self, $type ) = @_;

    # name, email, blog, company, location
    while (
        !(
            $type
            and ( grep { $_ eq $type } (qw/name email blog company location/) )
        )
      )
    {
        $type =
          $self->read('Update Key: (name, email, blog, company, location): ');
    }
    my $value = $self->read('Value: ');

    $self->run_github( 'user', 'update', $type, $value );
}

sub user_pub_keys {
    my ( $self, $type, $number, $key ) = @_;

    if ( $type eq 'show' ) {
        $self->run_github( 'user', 'keys' );
    }
    elsif ( $type eq 'add' ) {
        my ( $name, $keyv );
        unless ( $number and $key ) {
            $name = $self->read('Pub Key Name: ');
            $keyv = $self->read('Key: ');
        }
        else {
            $name = $number;
            $keyv = $key;
        }

        return $self->run_github( 'user', 'create_key',
            { title => $name, key => $keyv } );
    }
    elsif ( $type eq 'del' ) {
        unless ( $number and $number =~ /^\d+$/ ) {
            $self->print('unknown argument. u.pub_keys.del :number');
            return;
        }
        $self->run_github( 'user', 'delete_key', $number );
    }
}

1;

__END__

=pod

=head1 NAME

App::GitHub - GitHub Command Tools

=head1 VERSION

version 1.0.1

=head1 SYNOPSIS

    $ github.pl

     command   argument          description
     repo      :user :repo       set owner/repo, eg: 'fayland perl-app-github'
     login     :login :pass      authenticated as :login
     loadcfg                     authed by git config --global github.user|pass
     ?,h                         help
     q,exit,quit                 exit
    
    Repos
     r.show                      more in-depth information for the :repo
     r.list                      list out all the repositories for the :user
     r.watch                     watch repositories (auth required)
     r.unwatch                   unwatch repositories (auth required)
     r.fork                      fork a repository (auth required)
     r.create                    create a new repository (auth required)
     r.set_private               set a public repo private (auth required)
     r.set_public                set a private repo public (auth required)
     r.commit    :sha1           show a specific commit
    
    Issues
     i.list    open|closed       see a list of issues for a project
     i.view    :number           get data on an individual issue by number
     i.open                      open a new issue (auth required)
     i.close   :number           close an issue (auth required)
     i.reopen  :number           reopen an issue (auth required)
     i.edit    :number           edit an issue (auth required)
     i.comment :number
     i.label   add|del :num :label
                                 add/remove a label (auth required)
    
    Users
     u.show                      get extended information on user
     u.update                    update your users info (auth required)
     u.followers
     u.following
     u.follow  :user             follow :user (auth required)
     u.unfollow :user            unfollow :user (auth required)
     u.pub_keys                  Public Key Management (auth required)
     u.pub_keys.add
     u.pub_keys.del :number
    
    Objects
     o.tree    :tree_sha1        get the contents of a tree by tree sha
     o.trees   :tree_sha1        get the contents of a tree by tree sha and recursively descend down the tree
     o.blob    :sha1             get the data of a blob (tree, file or commits)
    
    Others
     r.show    :user :repo       more in-depth information for a repository
     r.list    :user             list out all the repositories for a user
     u.show    :user             get extended information on :user

=head1 DESCRIPTION

A command-line wrapper for L<Net::GitHub>

Repository: L<http://github.com/worr/perl-app-github/tree/master>

=head1 SEE ALSO

L<Net::GitHub>

=head1 AUTHORS

=over 4

=item *

Fayland Lam <fayland@gmail.com>

=item *

William Orr <will@worrbase.com>

=back

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Fayland Lam.

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.