Group
Extension

App-GitHubPullRequest/lib/App/GitHubPullRequest.pm

#!perl

use strict;
use warnings;
use feature qw(say state);

package App::GitHubPullRequest;
$App::GitHubPullRequest::VERSION = '0.6.0';
# ABSTRACT: Command-line tool to query GitHub pull requests

use JSON qw(decode_json encode_json);
use Carp qw(croak);
use Encode qw(encode_utf8);
use File::Spec;

use constant DEBUG => $ENV{'GIT_PR_DEBUG'} || 0;


sub new {
    my ($class) = @_;
    return bless {}, $class;
}


sub run {
    my ($self, @args) = @_;
    my $cmd = scalar @args ? shift @args : 'list';
    my $method = $self->can($cmd);
    return $self->$method(@args) if $method;
    return $self->help(@args);
}


sub help {
    my ($self, @args) = @_;
    print <<"EOM";
$0 [<command> <args> ...]

Where command is one of these:

  help              Show this page
  list [<state>]    Show all pull requests (state: open/closed)
  show <number>     Show details for the specified pull request
  patch <number>    Fetch a properly formatted patch for specified pull request
  checkout <number> Create tracking branch for specified pull request

  login [<user>] [<password>] [<two-factor-auth-token>]
                              Login to GitHub and receive an access token (deprecated)
  authorize                   Create a GitHub OAuth personal access token
  comment <number> [<text>]   Create a comment on the specified pull request
  close <number>              Close the specified pull request
  open <number>               Reopen the specified pull request

EOM
    return 1;
}


sub list {
    my ($self, $state) = @_;
    $state ||= 'open';
    my $remote_repo = _find_github_remote();
    my $prs = _api_read("/repos/$remote_repo/pulls?state=$state");
    say ucfirst($state) . " pull requests for '$remote_repo':";
    unless ( @$prs ) {
        say "No pull requests found.";
        return 0;
    }
    foreach my $pr ( @$prs ) {
        my $number = $pr->{"number"};
        my $title = encode_utf8( $pr->{"title"} );
        my $date = $pr->{"updated_at"} || $pr->{'created_at'};
        say join(" ", $number, $date, $title);
    }
    return 0;
}


sub show {
    my ($self, $number, @args) = @_;
    die("Please specify a pull request number.\n") unless $number;
    my $pr = $self->_fetch_one($number);
    die("Unable to fetch pull request $number.\n")
        unless defined $pr;
    {
        my $user = $pr->{'user'}->{'login'};
        my $title = encode_utf8( $pr->{"title"} );
        my $body = encode_utf8( $pr->{"body"} );
        my $date = $pr->{"updated_at"} || $pr->{'created_at'};
        say "Date:    $date";
        say "From:    $user";
        say "Subject: $title";
        say "Number:  $number";
        say "\n$body\n" if $body;
    }
    my $comments = _api_read( $pr->{'comments_url'} );
    foreach my $comment (@$comments) {
        my $user = $comment->{'user'}->{'login'};
        my $date = $comment->{'updated_at'} || $comment->{'created_at'};
        my $body = encode_utf8( $comment->{'body'} );
        say "-" x 79;
        say "Date: $date";
        say "From: $user";
        say "\n$body\n";
    }
    return 0;
}


sub patch {
    my ($self, $number) = @_;
    die("Please specify a pull request number.\n") unless $number;
    my $patch = _get_url(
        $self->_fetch_one($number)->{'patch_url'}
    );
    die("Unable to fetch patch for pull request $number.\n")
        unless defined $patch;
    print $patch;
    return 0;
}


sub checkout {
    my ($self, $number) = @_;
    $number =~ s{[^\d]}{}g if defined $number;
    die("Please specify a pull request number.\n") unless $number;
    my $pr = $self->_fetch_one($number);

    # Get required contributor branch info
    my $head_repo   = $pr->{'head'}->{'repo'}->{'clone_url'};
    my $head_branch = $pr->{'head'}->{'ref'};
    my $head_user   = $pr->{'head'}->{'user'}->{'login'};

    # Check if the remote already exists in our repo
    my $head_remote;
    foreach my $line ( _qx("git", "remote -v") ) {
        my ($remote, $url, $type) = split /\s+/, $line;
        next unless $type eq '(fetch)'; # only consider fetch remotes
        if ( $url eq $head_repo ) {
            $head_remote = $remote;
            last;
        }
    }

    if ( $head_remote ) {
        # Remote already exists, try to update its state
        unless ( _qx(qw(git branch --list -r), qq{"$head_remote/$head_branch"}) ) {
            # Create a new tracking branch, because one doesn't already exist
            my ($content, $rc) = _run_ext(
                qw(git remote set-branches),
                '--add',        # don't remove any other existing tracking branches
                $head_remote,   # our remote's name/alias
                $head_branch,   # the ref we want to track
            );
            die("git failed with error $rc when trying to add tracking branch"
              . " to existing remote.\n")
                if $rc != 0;
        }

        # Fetch changes from just added remote
        say "Fetching changes from '$head_remote/$head_branch'";
        my ($content, $rc) = _run_ext(
            qw(git fetch),
            $head_remote,
        );
        die("git failed with error $rc when trying to update remote.\n")
            if $rc != 0;
    }
    else {
        # Create and fetch the branch info if it doesn't exist already
        $head_remote = $head_user;
        my ($content, $rc) = _run_ext(
            qw(git remote add),
            '-f',                    # only fetch specific refs
            '-t', $head_branch,      # add only a ref for this ref, not all
            $head_remote,            # what we'll name our remote
            $head_repo,              # URL to the head repo
        );
        die("git failed with error $rc when trying to add remote.\n")
            if $rc != 0;
    }

    # Actually checkout the ref we just updated as pr/<number>
    my ($content, $rc) = _run_ext(
        qw(git checkout),
        '-b', "pr/$number",
        '--track', "$head_remote/$head_branch",
    );
    die("git failed with error $rc when trying to check out branch.\n")
        if $rc != 0;

    return 0;
}


sub close { ## no critic (Subroutines::ProhibitBuiltinHomonyms)
    my ($self, $number) = @_;
    die("Please specify a pull request number.\n") unless $number;
    my $pr = $self->_state($number, 'closed');
    die("Unable to close pull request $number.\n")
        unless defined $pr;
    say "Pull request $number now in state: " . $pr->{'state'};
    return 0;
}


sub open { ## no critic (Subroutines::ProhibitBuiltinHomonyms)
    my ($self, $number) = @_;
    die("Please specify a pull request number.\n") unless $number;
    my $pr = $self->_state($number, 'open');
    die("Unable to open pull request $number.\n")
        unless defined $pr;
    say "Pull request $number now in state: " . $pr->{'state'};
    return 0;
}


sub comment {
    my ($self, $number, $text) = @_;
    die("Please specify a pull request number.\n") unless $number;
    my $remote_repo = _find_github_remote();
    my $filename;
    unless ( $text ) {
        $filename = _tmpfile("$remote_repo-$number");
        # Find and execute editor on temporary file
        my $editor = $ENV{'EDITOR'};
        unless ( $editor ) {
            _require_binary('editor');
            $editor = 'editor';
        }
        system($editor, $filename);
        # Fetch text just edited (if any)
        if ( -r $filename ) {
            CORE::open my $fh, "<:encoding(UTF-8)", $filename or die "Can't open $filename: $!";
            $text = join "", <$fh>;
            CORE::close $fh;
        }
        # Abort if no text found
        unless ( $text ) {
            unlink $filename;
            die("Your comment is empty. Command aborted.\n");
        }
    }
    my $comment = eval {
        _api_create(
            "/repos/$remote_repo/issues/$number/comments",
            { "body" => $text },
        );
    };
    die($@ . ( defined $filename ? "Comment text saved in '$filename'. Please remove it manually." : "" ) ."\n")
        if $@; # most likely network error
    die("Unable to add comment on pull request $number.\n")
        unless defined $comment;
    say "Comment added. You can view it online here: " . $comment->{'html_url'};

    # Remove temporary file if everything went well
    if ( defined $filename and -e $filename ) {
        my $count = unlink $filename;
        warn("Unable to remove temporary file $filename: $!\n")
            unless $count;
    }

    return 0;
}


sub login {
    my ($self, $user, $password, $two_factor_token) = @_;

    # Add deprecation message
    say "\nThis authorization method is deprecated and will be removed on November 13, 2020.";
    say "Please use the 'authorize' command to authenticate with GitHub.\n";

    # Try to fetch user/password from git config (or prompt)
    $user     ||= _qx('git', "config github.user")     || _prompt('GitHub username');
    $password ||= _qx('git', "config github.password") || _prompt('GitHub password', 'hidden');
    die("Please specify a user name.\n") unless $user;
    die("Please specify a password.\n")  unless $password;
    # Prompt for two-factor auth token
    $two_factor_token ||= _prompt('GitHub two-factor authentication token (if any)');

    # Perform authentication
    my $auth = _api_create(
        "/authorizations",
        {
            "scopes"   => [qw( public_repo repo )],
            "note"     => __PACKAGE__,
            "note_url" => 'https://metacpan/module/' . __PACKAGE__,
        },
        $user,
        $password,
        $two_factor_token,
    );
    die("Unable to authenticate with GitHub.\n")
        unless defined $auth;
    my $token = $auth->{'token'};
    die("Authentication data does not include a token.\n")
        unless $token;

    # Store authorization token
    my ($content, $rc) = _run_ext(qw(git config --global github.pr-token), $token);
    die("git config returned message '$content' and code $rc when trying to store your token.\n")
        if $rc != 0;
    say "Access token stored successfully. Go to https://github.com/settings/tokens to revoke access.";
    return 0;
}


sub authorize {
    my ($self, $token) = @_;
    # Verify that you want to overwrite an existing token
    my $old_token = _qx('git', "config github.pr-token");
    if ( $old_token and not $token ) {
        say "You're already authorized.";
        my $q = _prompt("Do you want to generate a new token (y/N)") || "n";
        return 0 if lc($q) ne 'y';
    }
    # Give instructions and ask for token if not specified on command line
    unless ( $token ) {
        say "Go to https://github.com/settings/tokens/new and follow the directions to generate a new token.";
        say "Give the token a name of your choice, e.g. 'git-pr', and give it the 'repo' permission.";
        say "The 'public_repo' permission is enough if you only plan to use it with public repositories.\n";
        $token = _prompt('GitHub OAuth personal access token');
    }
    # Make sure a token is specified
    die("No token was specified. No changes have been made to your configuration.\n")
        unless $token;
    # Store authorization token
    my ($content, $rc) = _run_ext(qw(git config --global github.pr-token), $token);
    die("git config returned message '$content' and code $rc when trying to store your token.\n")
        if $rc != 0;
    say "Access token stored successfully. Go to https://github.com/settings/tokens to revoke access.";
    return 0;
}

sub _state {
    my ($self, $number, $state) = @_;
    croak("Please specify a pull request number") unless $number;
    croak("Please specify a pull request state") unless $state;
    my $remote_repo = _find_github_remote();
    return _api_update(
        "/repos/$remote_repo/pulls/$number",
        { "state" => $state },
    );
}

sub _fetch_one {
    my ($self, $number) = @_;
    my $remote_repo = _find_github_remote();
    return _api_read("/repos/$remote_repo/pulls/$number");
}

sub _find_github_remote {
    # Parse lines from git and use first found github repo
    my @repos;
    my $repo_map = {};
    foreach my $line ( _qx('git', 'remote -v') ) {
        my ($remote, $url, $type) = split /\s+/, $line;
        next unless $type eq '(fetch)'; # only consider fetch remotes
        next unless $url =~ m/github\.com/; # only consider remotes to github
        if ( $url =~ m{github.com[:/](.+?)(?:\.git)?$} ) {
            push @repos, $1;
            $repo_map->{$1} = $remote;
        }
    }

    # Allow override for testing
    @repos = ( $ENV{"GITHUB_REPO"} ) if $ENV{'GITHUB_REPO'};

    die("No valid GitHub repos found.\n")
        unless @repos;

    # Try each repo found in turn
    while ( my $repo = shift @repos ) {
        print "Fetching GitHub repo: $repo\n"
            if DEBUG;
        # Fetch repo information
        my ($repo_info, $code) = _api_read("/repos/$repo", 'return_on_error');

        # Skip repo which is not found
        if ( $code eq 404 ) {
            print STDERR "WARNING: Skipping invalid GitHub repo: $repo\n";
            if ( $repo_map->{$repo} ) {
                print STDERR "WARNING:\n";
                print STDERR "WARNING: Remove invalid git remote with command:\n";
                print STDERR "WARNING:   git remote remove $repo_map->{$repo}\n";
                print STDERR "WARNING:\n";
            }
            next;
        }

        # Return the parent repo if repo is a fork
        return $repo_info->{'parent'}->{'full_name'}
            if $repo_info->{'fork'};

        # Not a fork, use this repo
        return $repo;
    }

    die("No valid GitHub repo found. List of remotes exhausted.\n");
}

# Ask the user for some information
# Disable local echo if $hide_echo is true (for passwords)
sub _prompt {
    my ($label, $hide_echo) = @_;
    _echo('off') if $hide_echo;
    print "$label: " if defined $label;
    my $input = scalar <STDIN>;
    chomp $input;
    print "\n";
    _echo('on') if $hide_echo;
    return $input;
}

# Turn local echo on or off (Unix only for now)
sub _echo {
    my ($state) = @_;
    croak("Please specify an echo state of on or off") unless $state;

    # Test if we're on Unix (snatched from Platform::Unix)
    my $is_unix = $^O =~ /^(Linux|.*BSD.*|.*UNIX.*|Darwin|Solaris|SunOS|Haiku|Next|dec_osf|svr4|sco_sv|unicos.*|.*x)$/i;

    if ( $is_unix ) {
        return _run_ext(qw{stty -echo}) if $state eq 'off';
        return _run_ext(qw{stty echo})  if $state eq 'on';
    }

    # Don't know how to turn local echo on or off on other platforms.
    # If you happen to know how, please send a pull request with a fix
    return;
}

# Generate a random temporary filename with the given prefix
sub _tmpfile {
    my ($prefix) = @_;
    $prefix = defined $prefix ? "${prefix}-" : '';
    $prefix =~ s{[^A-Za-z0-9_-]}{-}g;
    srand($$ + time);
    my $random = $$;
    $random .= int(rand(10)) for 1..10;
    return "/tmp/git-pr-$prefix$random";
}

# Return stdout from external program
# If called in list context, each line will be chomped
# In scalar context, only last line will be chomped
sub _qx {
    my ($cmd, @rest) = @_;
    _require_binary($cmd);
    $cmd .= " " . join(" ", @rest) if @rest;
    warn("_qx: $cmd\n") if DEBUG;
    return map { chomp; $_ } qx{$cmd}
        if wantarray;
    my $content = qx{$cmd};
    chomp $content;
    return $content;
}

# Run an external command and return STDOUT and exit code
## no critic (Subroutines::RequireArgUnpacking)
sub _run_ext {
    croak("Please specify a command line") unless @_;
    my $cmd = join(" ", @_);
    my $prg = $_[0];
    warn("_run_ext: $cmd\n") if DEBUG;
    _require_binary($prg);
    CORE::open my $fh, "-|", @_ or die("Can't run command '$cmd': $!");
    my $stdout = join("", <$fh>);
    CORE::close $fh;
    my $rc = $? >> 8; # exit code, see perldoc perlvar for details
    return $stdout, $rc;
}
## use critic

# Make sure a program is present in path
sub _require_binary {
    my ($bin) = @_;
    croak("Please specify program to require") unless $bin;
    state %cache;
    unless (exists $cache{$bin}) {
        # Cache miss, let's see if we can find that program
        warn("Checking if '$bin' exists in path\n") if DEBUG;
        # Figure out path elements in a cross-platform way and go through
        # each and check if it has the executable program in it
        my @path = File::Spec->path();
        while (my $dir = shift @path) {
            my $file = File::Spec->catfile($dir, $bin);
            warn("Looking for executable bit on '$file'\n") if DEBUG;
            ## no critic (ValuesAndExpressions::ProhibitCommaSeparatedStatements)
            $cache{$bin} = $file, last if -x $file;
            ## use critic
            $cache{$bin} = 0;
        }
        warn("'$bin' was found at '$cache{$bin}'\n") if DEBUG and $cache{$bin};
    }
    return 1 if $cache{$bin};
    die("You need the program '$bin' in your path to use this feature.\n");
}

# Return the base GitHub API URL as mentioned
# on http://developer.github.com/v3/
sub _api_url {
    my ($url) = @_;
    my $prefix = 'https://api.github.com/';
    # If no URL specified, just return the API URL
    return $prefix unless defined $url;
    # If URL already looks like a GitHub API URL, do nothing
    return $url if _is_api_url($url);
    # Create an API URL out of a partial URL as
    $url =~ s{^/*}{}; # Remove initial slashes, if any
    return $prefix . $url;
}

# Check if a URL is a GitHub API URL
sub _is_api_url {
    my ($url) = @_;
    croak("Please specify a URL to verify") unless $url;
    my $prefix = _api_url();
    return 1 if index($url, $prefix) == 0;
    return 0;
}

# Perform an API GET request
sub _api_read {
    my ($url, $return_on_error) = @_;
    my ($response, $code) = _get_url(
        _api_url($url),
        $return_on_error,
    );
    return decode_json($response), $code
        if $return_on_error;
    return decode_json($response);
}

# Perform an API POST request
sub _api_create {
    my ($url, $data, @rest) = @_;
    return decode_json(
        _post_url(
            _api_url($url),
            'application/json',
            encode_json($data),
            @rest,
        )
    );
}

# Perform an API PATCH request
sub _api_update {
    my ($url, $data) = @_;
    return decode_json(
        _patch_url(
            _api_url($url),
            'application/json',
            encode_json($data),
        )
    );
}

# Perform HTTP GET
sub _get_url {
    my ($url, $return_on_error) = @_;
    croak("Please specify a URL") unless $url;

    # See if we should use credentials
    my @credentials;
    if ( _is_api_url($url) ) {
        my $token = _qx('git', 'config github.pr-token');
        @credentials = ( '-H', "Authorization: token $token" ) if $token;
    }

    # Send request
    my ($content, $rc) = _run_ext(
        'curl',
        '-L',                            # follow redirects
        '-s',                            # be silent
        '-w', '%{http_code}',            # include HTTP status code at end of stdout
        @credentials,                    # Logon credentials, if any
        $url,                            # The URL we're GETing
    );
    die("curl failed to fetch $url with code $rc.\n") if $rc != 0;

    my $code = substr($content, -3, 3, '');

    return $content, $code if $return_on_error;

    if ( $code >= 400 ) {
        die("Fetching URL $url failed with code $code:\n$content");
    }

    return $content;
}

# Perform HTTP PATCH
sub _patch_url {
    my ($url, $mimetype, $data) = @_;
    croak("Please specify a URL") unless $url;
    croak("Please specify a mimetype") unless $mimetype;
    croak("Please specify some data") unless $data;

    # See if we should use credentials
    my @credentials;
    if ( _is_api_url($url) ) {
        my $token = _qx('git', 'config github.pr-token');
        die("You must login before you can modify information.\n")
            unless $token;
        @credentials = ( '-H', "Authorization: token $token" );
    }

    # Send request
    my ($content, $rc) = _run_ext(
        'curl',
        '-s',                            # be silent
        '-w', '%{http_code}',            # include HTTP status code at end of stdout
        '-X', 'PATCH',                   # perform an HTTP PATCH
        '-H', "Content-Type: $mimetype", # What kind of data we're sending
        '-d', $data,                     # Our data
        @credentials,                    # Logon credentials, if any
        $url,                            # The URL we're PATCHing
    );
    die("curl failed to patch $url with code $rc.\n") if $rc != 0;

    my $code = substr($content, -3, 3, '');
    if ( $code >= 400 ) {
        die("If you get 'Validation Failed' error without any reason,"
          . " most likely the pull request has already been merged or closed by the repo owner.\n"
          . "URL: $url\n"
          . "Code: $code\n"
          . $content
        ) if $code == 422;
        die("Patching URL $url failed with code $code:\n$content");
    }

    return $content;
}

# Perform HTTP POST
sub _post_url {
    my ($url, $mimetype, $data, $user, $password, $two_factor_token) = @_;
    croak("Please specify a URL") unless $url;
    croak("Please specify a mimetype") unless $mimetype;
    croak("Please specify some data") unless $data;

    # See if we should use credentials
    my @credentials;
    if ( _is_api_url($url) ) {
        my $token = _qx('git', 'config github.pr-token');
        die("You must login before you can modify information.\n")
            unless $token or ( $user and $password );
        if ( $user and $password ) {
            @credentials = ( '-u', "$user:$password" );
            push @credentials, '-H', "X-GitHub-OTP: $two_factor_token"
                if $two_factor_token;
        }
        else {
            @credentials = ( '-H', "Authorization: token $token" ) if $token;
        }
    }

    # Send request
    my ($content, $rc) = _run_ext(
        'curl',
        '-s',                            # be silent
        '-w', '%{http_code}',            # include HTTP status code at end of stdout
        '-X', 'POST',                    # perform an HTTP POST
        '-H', "Content-Type: $mimetype", # What kind of data we're sending
        '-d', $data,                     # Our data
        @credentials,                    # Logon credentials, if any
        $url,                            # The URL we're POSTing to
    );
    die("curl failed to post to $url with code $rc.\n") if $rc != 0;

    my $code = substr($content, -3, 3, '');
    if ( $code >= 400 ) {
        die("Posting to URL $url failed with code $code:\n$content");
    }

    return $content;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

App::GitHubPullRequest - Command-line tool to query GitHub pull requests

=head1 VERSION

version 0.6.0

=head1 SYNOPSIS

    $ git pr
    $ git pr list closed # not shown by default
    $ git pr show 7      # also includes comments
    $ git pr patch 7     # can be piped to colordiff if you like colors
    $ git pr checkout 7  # create upstream tracking branch pr/7
    $ git pr help

    $ git pr authorize   # Get access token for commands below
    $ git pr close 7
    $ git pr open 7
    $ git pr comment 7 'This is good stuff!'

=head1 INSTALLATION

Install it by just typing in these few lines in your shell:

    $ curl -L http://cpanmin.us | perl - --self-upgrade
    $ cpanm App::GitHubPullRequest

The following external programs are required:

=over 4

=item *

L<git(1)>

=item *

L<curl(1)>

=item *

L<stty(1)>

=back

=head1 COMMANDS

=head2 help

Displays some help.

=head2 list [<state>]

Shows all pull requests in the given state. State can be either C<open> or
C<closed>.  The default state is C<open>.  This is the default command if
none is specified.

=head2 show <number>

Shows details about the specified pull request number. Also includes
comments.

=head2 patch <number>

Shows the patch associated with the specified pull request number.

=head2 checkout <number>

Checks out the specified pull request in a dedicated tracking branch.
If the remote repo is not already specified in your git config, it will be
added and the branch in question will be fetched.

=head2 close <number>

Closes the specified pull request number. Be aware, you can't close a pull
request that has already been merged.  If you try to, you'll get an obscure
C<Validation Failed> error message from the GitHub API.

=head2 open <number>

Reopens the specified pull request number. Be aware, you can't reopen a pull
request that has already been merged or closed by the repo owner.  If you
try to, you'll get an obscure C<Validation Failed> error message from the
GitHub API.

=head2 comment <number> [<text>]

Creates a comment on the specified pull request with the specified text. If
text is not specified, an editor will be opened for you to type in the text.

If your C<EDITOR> environment variable has been set, that editor is used to
edit the text.  If it has not been set, it will try to use the L<editor(1)>
external program.  This is usually a symlink set up by your operating system
to the most recently installed text editor.

The text must be encoded using UTF-8.

=head2 login [<user>] [<password>] [<2fa-token>]

DEPRECATED: Logs you in to GitHub and creates a new access token used
instead of your password and two-factor authentication token. If you don't
specify either of the options, they are looked up in your git config github
section. If none of those are found, you'll be prompted for them.

=head2 authorize [<access-token>]

Logs you in to GitHub by manually creating an OAuth personal access token.
Follow the directions on-screen to generate one and insert it when prompted.
If you already have an access token you want to use you can also specify it
on the command line.

=head1 METHODS

=head2 new

Constructor. Takes no parameters.

=head2 run(@args)

Calls any of the other listed public methods with specified arguments. This
is usually called automatically when you invoke L<git-pr>.

=head1 DEBUGGING

Set the environment variable GIT_PR_DEBUG to a non-zero value to see more
details, like each API command being executed.

If you want to interact with another GitHub repo than the one in your
current directory, set the environment variable GITHUB_REPO to the name of
the repo in question. Example:

    GITHUB_REPO=robinsmidsrod/App-GitHubPullRequest git pr list

Be aware, that if that repo is a fork, the program will look for its parent.

=head1 CAVEATS

If you don't authenticate with GitHub using the authorize command, it will use
unauthenticated API requests where possible, which has a rate-limit of 60
requests.  If you authorize first it should allow 5000 requests before you hit
the limit.

You must be standing in a directory that is a git dir and that directory must
have a remote that points to github.com for the tool to work.

=head1 SEE ALSO

=over 4

=item *

L<git-pr>

=item *

L<GitHub Pull Request documentation|https://help.github.com/articles/using-pull-requests>

=item *

L<GitHub Pull Request API documentation|http://developer.github.com/v3/pulls/>

=back

=head1 SEMANTIC VERSIONING

This module uses semantic versioning concepts from L<http://semver.org/>.

=for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan

=head1 SUPPORT

=head2 Perldoc

You can find documentation for this module with the perldoc command.

  perldoc App::GitHubPullRequest

=head2 Websites

The following websites have more information about this module, and may be of help to you. As always,
in addition to those websites please use your favorite search engine to discover more resources.

=over 4

=item *

MetaCPAN

A modern, open-source CPAN search engine, useful to view POD in HTML format.

L<http://metacpan.org/release/App-GitHubPullRequest>

=item *

Search CPAN

The default CPAN search engine, useful to view POD in HTML format.

L<http://search.cpan.org/dist/App-GitHubPullRequest>

=item *

RT: CPAN's Bug Tracker

The RT ( Request Tracker ) website is the default bug/issue tracking system for CPAN.

L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-GitHubPullRequest>

=item *

AnnoCPAN

The AnnoCPAN is a website that allows community annotations of Perl module documentation.

L<http://annocpan.org/dist/App-GitHubPullRequest>

=item *

CPAN Ratings

The CPAN Ratings is a website that allows community ratings and reviews of Perl modules.

L<http://cpanratings.perl.org/d/App-GitHubPullRequest>

=item *

CPAN Forum

The CPAN Forum is a web forum for discussing Perl modules.

L<http://cpanforum.com/dist/App-GitHubPullRequest>

=item *

CPANTS

The CPANTS is a website that analyzes the Kwalitee ( code metrics ) of a distribution.

L<http://cpants.cpanauthors.org/dist/App-GitHubPullRequest>

=item *

CPAN Testers

The CPAN Testers is a network of smokers who run automated tests on uploaded CPAN distributions.

L<http://www.cpantesters.org/distro/A/App-GitHubPullRequest>

=item *

CPAN Testers Matrix

The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.

L<http://matrix.cpantesters.org/?dist=App-GitHubPullRequest>

=item *

CPAN Testers Dependencies

The CPAN Testers Dependencies is a website that shows a chart of the test results of all dependencies for a distribution.

L<http://deps.cpantesters.org/?module=App::GitHubPullRequest>

=back

=head2 Bugs / Feature Requests

Please report any bugs or feature requests by email to C<bug-app-githubpullrequest at rt.cpan.org>, or through
the web interface at L<https://rt.cpan.org/Public/Bug/Report.html?Queue=App-GitHubPullRequest>. You will be automatically notified of any
progress on the request by the system.

=head2 Source Code

The code is open to the world, and available for you to hack on. Please feel free to browse it and play
with it, or whatever. If you want to contribute patches, please send me a diff or prod me to pull
from your repository :)

L<https://github.com/robinsmidsrod/App-GitHubPullRequest>

  git clone git://github.com/robinsmidsrod/App-GitHubPullRequest.git

=head1 AUTHOR

Robin Smidsrød <robin@smidsrod.no>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2020 by Robin Smidsrød.

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.