github_relabel.pl 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. #!/usr/bin/env perl
  2. use strict;
  3. use warnings;
  4. use HTTP::Tiny;
  5. use IO::Socket::SSL 1.52;
  6. use utf8;
  7. use Getopt::Long;
  8. my $Base_URL = "https://api.github.com/repos/";
  9. my $User_Repo = 'elastic/elasticsearch/';
  10. my $Issue_URL = "https://github.com/${User_Repo}issues";
  11. use JSON();
  12. use URI();
  13. use URI::Escape qw(uri_escape_utf8);
  14. our $json = JSON->new->utf8(1);
  15. our $http = HTTP::Tiny->new(
  16. default_headers => {
  17. Accept => "application/vnd.github.v3+json",
  18. Authorization => load_github_key()
  19. }
  20. );
  21. my %Opts = ( state => 'open' );
  22. GetOptions(
  23. \%Opts, #
  24. 'state=s', 'labels=s', 'add=s', 'remove=s'
  25. ) || exit usage();
  26. die usage('--state must be one of open|all|closed')
  27. unless $Opts{state} =~ /^(open|all|closed)$/;
  28. die usage('--labels is required') unless $Opts{labels};
  29. die usage('Either --add or --remove is required')
  30. unless $Opts{add} || $Opts{remove};
  31. relabel();
  32. #===================================
  33. sub relabel {
  34. #===================================
  35. my @remove = split /,/, ( $Opts{remove} || '' );
  36. my @add = split /,/, ( $Opts{add} || '' );
  37. my $add_json = $json->encode( \@add );
  38. my $url = URI->new( $Base_URL . $User_Repo . 'issues' );
  39. $url->query_form(
  40. state => $Opts{state},
  41. labels => $Opts{labels},
  42. per_page => 100
  43. );
  44. my $spool = Spool->new($url);
  45. while ( my $issue = $spool->next ) {
  46. my $id = $issue->{number};
  47. print "$Issue_URL/$id\n";
  48. if (@add) {
  49. add_label( $id, $add_json );
  50. }
  51. for (@remove) {
  52. remove_label( $id, $_ );
  53. }
  54. }
  55. print "Done\n";
  56. }
  57. #===================================
  58. sub add_label {
  59. #===================================
  60. my ( $id, $json ) = @_;
  61. my $response = $http->post(
  62. $Base_URL . $User_Repo . "issues/$id/labels",
  63. { content => $json,
  64. headers => { "Content-Type" => "application/json; charset=utf-8" }
  65. }
  66. );
  67. die "$response->{status} $response->{reason}\n"
  68. unless $response->{success};
  69. }
  70. #===================================
  71. sub remove_label {
  72. #===================================
  73. my ( $id, $name ) = @_;
  74. my $url
  75. = $Base_URL
  76. . $User_Repo
  77. . "issues/$id/labels/"
  78. . uri_escape_utf8($name);
  79. my $response = $http->delete($url);
  80. die "$response->{status} $response->{reason}\n"
  81. unless $response->{success};
  82. }
  83. #===================================
  84. sub load_github_key {
  85. #===================================
  86. my ($file) = glob("~/.github_auth");
  87. unless ( -e $file ) {
  88. warn "File ~/.github_auth doesn't exist - using anonymous API. "
  89. . "Generate a Personal Access Token at https://github.com/settings/applications\n";
  90. return '';
  91. }
  92. open my $fh, $file or die "Couldn't open $file: $!";
  93. my ($key) = <$fh> || die "Couldn't read $file: $!";
  94. $key =~ s/^\s+//;
  95. $key =~ s/\s+$//;
  96. die "Invalid GitHub key: $key"
  97. unless $key =~ /^[0-9a-f]{40}$/;
  98. return "token $key";
  99. }
  100. #===================================
  101. sub usage {
  102. #===================================
  103. my $msg = shift || '';
  104. if ($msg) {
  105. $msg = "\nERROR: $msg\n\n";
  106. }
  107. return $msg . <<"USAGE";
  108. $0 --state=open|closed|all --labels=foo,bar --add=new1,new2 --remove=old1,old2
  109. USAGE
  110. }
  111. package Spool;
  112. use strict;
  113. use warnings;
  114. #===================================
  115. sub new {
  116. #===================================
  117. my $class = shift;
  118. my $url = shift;
  119. return bless {
  120. url => $url,
  121. buffer => []
  122. },
  123. $class;
  124. }
  125. #===================================
  126. sub next {
  127. #===================================
  128. my $self = shift;
  129. if ( @{ $self->{buffer} } == 0 ) {
  130. $self->refill;
  131. }
  132. return shift @{ $self->{buffer} };
  133. }
  134. #===================================
  135. sub refill {
  136. #===================================
  137. my $self = shift;
  138. return unless $self->{url};
  139. my $response = $http->get( $self->{url} );
  140. die "$response->{status} $response->{reason}\n"
  141. unless $response->{success};
  142. $self->{url} = '';
  143. if ( my $link = $response->{headers}{link} ) {
  144. my @links = ref $link eq 'ARRAY' ? @$link : $link;
  145. for ($link) {
  146. next unless $link =~ /<([^>]+)>; rel="next"/;
  147. $self->{url} = $1;
  148. last;
  149. }
  150. }
  151. push @{ $self->{buffer} }, @{ $json->decode( $response->{content} ) };
  152. }