#!/usr/bin/env perl use strict; use warnings; use HTTP::Tiny; use IO::Socket::SSL 1.52; use utf8; use Getopt::Long; my $Base_URL = "https://api.github.com/repos/"; my $User_Repo = 'elastic/elasticsearch/'; my $Issue_URL = "https://github.com/${User_Repo}issues"; use JSON(); use URI(); use URI::Escape qw(uri_escape_utf8); our $json = JSON->new->utf8(1); our $http = HTTP::Tiny->new( default_headers => { Accept => "application/vnd.github.v3+json", Authorization => load_github_key() } ); my %Opts = ( state => 'open' ); GetOptions( \%Opts, # 'state=s', 'labels=s', 'add=s', 'remove=s' ) || exit usage(); die usage('--state must be one of open|all|closed') unless $Opts{state} =~ /^(open|all|closed)$/; die usage('--labels is required') unless $Opts{labels}; die usage('Either --add or --remove is required') unless $Opts{add} || $Opts{remove}; relabel(); #=================================== sub relabel { #=================================== my @remove = split /,/, ( $Opts{remove} || '' ); my @add = split /,/, ( $Opts{add} || '' ); my $add_json = $json->encode( \@add ); my $url = URI->new( $Base_URL . $User_Repo . 'issues' ); $url->query_form( state => $Opts{state}, labels => $Opts{labels}, per_page => 100 ); my $spool = Spool->new($url); while ( my $issue = $spool->next ) { my $id = $issue->{number}; print "$Issue_URL/$id\n"; if (@add) { add_label( $id, $add_json ); } for (@remove) { remove_label( $id, $_ ); } } print "Done\n"; } #=================================== sub add_label { #=================================== my ( $id, $json ) = @_; my $response = $http->post( $Base_URL . $User_Repo . "issues/$id/labels", { content => $json, headers => { "Content-Type" => "application/json; charset=utf-8" } } ); die "$response->{status} $response->{reason}\n" unless $response->{success}; } #=================================== sub remove_label { #=================================== my ( $id, $name ) = @_; my $url = $Base_URL . $User_Repo . "issues/$id/labels/" . uri_escape_utf8($name); my $response = $http->delete($url); die "$response->{status} $response->{reason}\n" unless $response->{success}; } #=================================== sub load_github_key { #=================================== my ($file) = glob("~/.github_auth"); unless ( -e $file ) { warn "File ~/.github_auth doesn't exist - using anonymous API. " . "Generate a Personal Access Token at https://github.com/settings/applications\n"; return ''; } open my $fh, $file or die "Couldn't open $file: $!"; my ($key) = <$fh> || die "Couldn't read $file: $!"; $key =~ s/^\s+//; $key =~ s/\s+$//; die "Invalid GitHub key: $key" unless $key =~ /^[0-9a-f]{40}$/; return "token $key"; } #=================================== sub usage { #=================================== my $msg = shift || ''; if ($msg) { $msg = "\nERROR: $msg\n\n"; } return $msg . <<"USAGE"; $0 --state=open|closed|all --labels=foo,bar --add=new1,new2 --remove=old1,old2 USAGE } package Spool; use strict; use warnings; #=================================== sub new { #=================================== my $class = shift; my $url = shift; return bless { url => $url, buffer => [] }, $class; } #=================================== sub next { #=================================== my $self = shift; if ( @{ $self->{buffer} } == 0 ) { $self->refill; } return shift @{ $self->{buffer} }; } #=================================== sub refill { #=================================== my $self = shift; return unless $self->{url}; my $response = $http->get( $self->{url} ); die "$response->{status} $response->{reason}\n" unless $response->{success}; $self->{url} = ''; if ( my $link = $response->{headers}{link} ) { my @links = ref $link eq 'ARRAY' ? @$link : $link; for ($link) { next unless $link =~ /<([^>]+)>; rel="next"/; $self->{url} = $1; last; } } push @{ $self->{buffer} }, @{ $json->decode( $response->{content} ) }; }