Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 338 lines (246 sloc) 7.75 KB
#!/usr/bin/perl
use strict;
use warnings;
use 5.8.0;
use Net::Google::Code '0.19';
use Net::GitHub '0.21';
use Getopt::Long;
use Pod::Usage;
use Carp qw(confess);
=head1 NAME
gcode-issue-import - Import issues from Google Code into Github's issue tracker
=head1 SYNOPSIS
gcode-issue-import \
--from your_googlecode_projectname \
--to owner/githubreponame
=head1 DESCRIPTION
This script aims to pull as much data as possible from Google Code's issue
tracker and insert it into Github's issue tracker.
This should be considered ALPHA-QUALITY CODE . Do not (yet) run it targeting a
Github issue tracker that contains data you care about, or you may spend days
manually deleting ugly tickets.
=head1 OPTIONS
=over 4
=item --from <google code project name>
The project name of your Google Code project. Mandatory.
=item --to <github user/repository>
The target Github repository, in user/repositoryname format. Mandatory.
=item --github-login <username>
Your Github login name. Optional. If not provided, value will be pulled from
your git configuration with `git config`
=item --github-token <api token>
Your Github API token. Optional. If not provided, value will be pulled from
your git configuration with `git config`
=item --start-index <integer>
Google Code issue ID to start at. Optional. If not provided, defaults to 1.
=item --help
Short-form help
=item --man
Full manpage
=back
=head1 SEE ALSO
=over 4
=item *
google-code-to-github-issues.pl by Tatsuhiko Miyagawa
(L<http://remediecode.org/2009/04/moved-issues-to-github.html>), which was the basis for this script
=item *
L<App::SD|App::SD>, which purports to do bidirectional replication of issues
between various bugtrackers, including Google Code and Github. It didn't work
for me, however.
=back
=head1 AUTHORS
Dave O'Neill <dmo@dmo.ca>
Based on google-code-to-github-issues.pl by Tatsuhiko Miyagawa
(L<http://remediecode.org/2009/04/moved-issues-to-github.html>)
=head1 LICENSE
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=cut
our $VERSION = '0.100';
my($gc_proj, $gh_proj, $start_index, $max_results);
my %github;
GetOptions(
'from=s' => \$gc_proj,
'to=s' => \$gh_proj,
'github-login=s' => \$github{'login'},
'github-token=s' => \$github{'token'},
'start-index=i' => \$start_index,
'help' => sub { pod2usage( -exitval => 0, -verbose => 1 ) },
'man' => sub { pod2usage( -exitval => 0, -verbose => 2 ) },
) || pod2usage( -exitval => 1, -verbose => 0 );
if( !$gc_proj ) {
pod2usage( -message => 'You must specify a Google Code project with --from');
}
if( !$gh_proj || $gh_proj !~ m{^[^/]+/[^/]+$}) {
pod2usage( -message => 'You must specify a valid-looking Github repository with --to');
}
$start_index ||= 1;
$max_results = 1_000; # TODO: configureable, and/or loop with smaller max until we fetch all
@github{qw( owner repo )} = split '/', $gh_proj;
chomp($github{login} = `git config github.user`) unless $github{login};
chomp($github{token} = `git config github.token`) unless $github{token};
my $code = Net::Google::Code->new(project => $gc_proj);
my $github = Net::GitHub->new(%github);
my @gcode_closed_statii = qw(
Fixed
Verified
Invalid
Duplicate
WontFix
);
for my $issue ( $code->issue->list(can => 'all', start_index => $start_index, max_results => $max_results ) ) {
$issue->load_comments();
import_issue( $github, $issue );
}
# die()'s on ANY error we know about. Caller must catch.
sub import_issue
{
my ($github, $issue) = @_;
my $desc_with_user =
"Originally filed by " . $issue->reporter . " on " . $issue->reported
. "\n\n"
. $issue->description();
my $gh_issue = Private::Net::GitHub::Issue->new({
github => $github,
title => $issue->summary,
body => $desc_with_user,
});
print 'Google Code issue '
. $issue->id
. ' (' . $issue->summary
. ') created as Github issue '
. $gh_issue->{number}
. "\n";
# Labels
foreach my $label ( @{ $issue->labels } ) {
$gh_issue->add_label( $label );
}
# Comments
$issue->load_comments();
foreach my $comment (@{ $issue->comments }) {
my @update_info = ();
if( $comment->updates ) {
# Can't handle change in ownership via Net::GitHub
if (exists $comment->updates->{owner} ) {
my $o = delete $comment->updates->{owner};
push(@update_info, "Original ticket set owner to $o");
}
if (exists $comment->updates->{labels} ) {
my $l = delete $comment->updates->{labels};
foreach ( @{ $l } ) {
if( s/^-// ) {
$gh_issue->remove_label( $_ );
push(@update_info, "Removed label $_");
} else {
$gh_issue->add_label( $_ );
push(@update_info, "Added label $_");
}
}
}
if( exists $comment->updates->{status} ) {
my $s = delete $comment->updates->{status};
$gh_issue->set_status( $s );
push(@update_info, "Original ticket set status to $s (we converted to $gh_issue->{state})");
}
if( exists $comment->updates->{summary} ) {
my $s = delete $comment->updates->{summary};
my $old_title = $gh_issue->{title};
$gh_issue->edit( $s );
push(@update_info, "Title changed from '$old_title' to '$s'");
}
# Warn of missed updates
foreach my $unhandled (keys %{$comment->updates}) {
warn "Didn't handle update key $unhandled";
}
}
next unless ($comment->content || scalar @update_info);
my $comment_with_user =
"Updated by "
. $comment->author
. " on "
. $comment->date;
if( $comment->content ) {
$comment_with_user .= "\n\n" . $comment->content;
}
if( @update_info ) {
$comment_with_user .= "\n\n" . join("\n", @update_info);
}
$gh_issue->comment( $comment_with_user );
}
# Set the current ticket status, if changed
my $old_state = $gh_issue->{state};
$gh_issue->set_status( $issue->status );
if( $old_state ne $gh_issue->{state} ) {
my $s = $issue->status;
$gh_issue->comment("Original ticket set status to $s (we converted to $gh_issue->{state})");
}
# TODO attachments?
}
package Private::Net::GitHub::Issue;
use Carp qw(confess);
use URI::Escape;
sub _throttle_request_rate
{
# Sleep at least 1s per request to keep us under 60 per minute as per
# API docs.
sleep(1);
}
sub _die_if_error
{
my ($return) = @_;
confess "got error: $return->{error}" if (ref($return) eq 'HASH' && exists $return->{error});
return 1;
}
sub new
{
my ($class, $args) = @_;
my $gi = $args->{github}->issue();
_throttle_request_rate();
my $self = $gi->open($args->{title}, $args->{body});
_die_if_error( $self );
if( ! $self->{number} ) {
confess "Didn't get an incident number from ->open";
}
$self->{_gi} = $gi;
bless $self, $class;
}
sub add_label
{
my ($self, $label) = @_;
_throttle_request_rate();
_die_if_error( $self->{_gi}->add_label($self->{number}, uri_escape($label, "^A-Za-z0-9\-_~") ) );
}
sub remove_label
{
my ($self, $label) = @_;
_throttle_request_rate();
_die_if_error( $self->{_gi}->remove_label($self->{number}, uri_escape($label, "^A-Za-z0-9\-_~" ) ) );
}
sub edit
{
my ($self, $title, $body) = @_;
$title = $self->{title} unless defined $title;
$body = $self->{body} unless defined $body;
_throttle_request_rate();
_die_if_error( $self->{_gi}->edit($self->{number}, $title, $body ) );
$self->{title} = $title;
return 1;
}
sub comment
{
my ($self, $comment) = @_;
_throttle_request_rate();
_die_if_error( $self->{_gi}->comment($self->{number}, $comment ) );
}
sub set_status
{
my ($self, $gcode_status) = @_;
_throttle_request_rate();
if( grep { $_ eq $gcode_status } @gcode_closed_statii ) {
_die_if_error( $self->{_gi}->close( $self->{number} ) ) unless $self->{state} eq 'closed';
$self->{state} = 'closed';
} else {
_die_if_error( $self->{_gi}->reopen( $self->{number} ) ) unless $self->{state} eq 'open';
$self->{state} = 'open';
}
}