App-GitHubPullRequest
view release on metacpan or search on metacpan
lib/App/GitHubPullRequest.pm view on Meta::CPAN
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];
lib/App/GitHubPullRequest.pm view on Meta::CPAN
=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
( run in 1.251 second using v1.01-cache-2.11-cpan-6aa56a78535 )