package GitHubBackup::API; use strict; use warnings; use utf8; use Carp qw(croak); use LWP::UserAgent; use JSON; sub new { my $class = shift; my $args = shift; return bless $args, $class; } sub json_api { my $self = shift; my $url = shift; my $ua = LWP::UserAgent->new; my $json = JSON->new->utf8->indent; my $res = $ua->get( "https://api.github.com$url" ); $res->is_success or croak $res->status_line; return $json->decode($res->content); } sub get { my $self = shift; my $url = shift; my %parameters = @_; if ($self->access_token) { $parameters{access_token} = $self->access_token; } my $parameters = ''; while (my($key, $value) = each %parameters) { $parameters .= "&$key=$value"; } my $page = 1; my $data = []; while(1) { my $result = $self->json_api("$url?per_page=100&page=$page$parameters"); if (ref($result) eq 'ARRAY' && scalar @$result > 0) { push @$data, @$result; $page++; next; } last; } return $data; } sub access_token { my $self = shift; return $self->{access_token}->(); } package GitHubBackup; use strict; use warnings; use utf8; use Carp qw(croak); use File::Spec; # both hash and hashref are acceptable sub new { my $class = shift; my $args = (ref $_[0] eq 'HASH') ? $_[0] : {@_}; return bless $args, $class; } sub account { my $self = shift; my $args = shift; if (defined $args) { $self->{repos} = undef; $self->{account} = $args; } return $self->{account}; } sub repository { my $self = shift; my $args = shift; if (defined $args) { $self->{repos} = undef; $self->{repository} = $args; } return $self->{repository}; } sub directory { my $self = shift; my $args = shift; if (defined $args) { $self->{directory} = File::Spec->rel2abs($args); } return $self->{directory}; } sub access_token { my $self = shift; my $args = shift; if (defined $args) { $self->{access_token} = $args; } return $self->{access_token}; } sub api { my $self = shift; unless ($self->{api}) { $self->{api} = GitHubBackup::API->new({ access_token => sub {$self->access_token}, }); } return $self->{api}; } sub repos { my $self = shift; return $self->{repos} if ($self->{repos}); $self->{repos} = []; my $account = $self->account or croak "account is not set"; my $token = ($self->access_token) ? "?access_token=" . $self->access_token : ''; my $result; if (my $repository = $self->repository) { $result = [ $self->api->json_api("/repos/$account/$repository$token") ]; } else { $result = $self->api->get("/users/$account/repos"); } foreach my $repos (@$result) { push @{$self->{repos}}, GitHubBackup::Repository->new({ directory => sub {$self->directory}, api => sub {$self->api}, repos => $repos, }) ; } return $self->{repos}; } sub backup { my $self = shift; foreach my $repos (@{$self->repos}) { $repos->backup; } return $self; } package GitHubBackup::Repository; use strict; use warnings; use utf8; use Carp qw(croak); use Git::Repository; use File::chdir; use File::Spec; use File::Path qw(mkpath); use LWP::UserAgent; use JSON; sub new { my $class = shift; my $args = shift; return bless $args, $class; } sub clone_url { return (shift)->{repos}{clone_url}; } sub full_name { return (shift)->{repos}{full_name}; } sub has_downloads { return (shift)->{repos}{has_downloads}; } sub forks_count { return (shift)->{repos}{forks_count}; } sub has_wiki { return (shift)->{repos}{has_wiki}; } sub has_issues { return (shift)->{repos}{has_issues}; } sub directory { my $self = shift; my $path = $self->full_name; if (my $base = $self->{directory}->()) { $path = File::Spec->catfile($base, $path); } return $path; } sub api { my $self = shift; return $self->{api}->(); } sub message { my $self = shift; my $message = shift; print $self->full_name, " $message\n"; return $self; } sub sync { my $self = shift; my $url = shift; my $dir = shift; if (-d "$dir") { local $CWD = $dir; $self->message("fetch --all $dir"); Git::Repository->run(fetch => '--all'); return $self; } $self->message("clone --mirror $dir"); mkpath $dir; Git::Repository->run(clone => '--mirror' => $url => $dir); return $self; } sub clone_git { my $self = shift; my $dir = $self->directory . '.git'; my $url = $self->clone_url; $self->sync($url => $dir); return $self; } sub forks { my $self = shift; return $self->{forks} if ($self->{forks}); $self->{forks} = $self->api->get("/repos/" . $self->full_name . "/forks"); return $self->{forks}; } sub set_forks { my $self = shift; my $dir = $self->directory . '.git'; local $CWD = $dir; my $remotes = Git::Repository->run(branch => '--remotes'); my @fetch; foreach my $fork (@{$self->forks}) { if ($remotes =~ /$fork->{full_name}/) { $self->message("have ". $fork->{full_name}); next; } $self->message("remote add ". $fork->{full_name}); Git::Repository->run(remote => add => $fork->{full_name} => $fork->{clone_url}); push @fetch, $fork->{full_name}; } foreach my $fork (@fetch) { $self->message("fetch $fork"); Git::Repository->run(fetch => $fork); } return $self; } sub clone_wiki { my $self = shift; my $dir = $self->directory . '.wiki.git'; my $url = 'https://github.com/' . $self->full_name . '.wiki.git'; my $ua = LWP::UserAgent->new(max_redirect => 0); my $res = $ua->head( 'https://github.com/' . $self->full_name . '/wiki' ); if ($res->code != 200) { $self->message("wiki does not exist"); return $self; } $self->sync($url => $dir); return $self; } sub issues { my $self = shift; return $self->{issues} if ($self->{issues}); my $open = $self->api->get("/repos/" . $self->full_name . "/issues"); my $closed = $self->api->get("/repos/" . $self->full_name . "/issues", state => 'closed'); if ($open) { push @{$self->{issues}}, @$open } if ($closed) { push @{$self->{issues}}, @$closed } return $self->{issues}; } sub save_issues { my $self = shift; my $ua = LWP::UserAgent->new; my $json = JSON->new->utf8->indent; my $dir = $self->directory . '.issues'; mkpath $dir unless (-d $dir); local $CWD = $dir; foreach my $issue (@{$self->issues}) { my $number = $issue->{number}; $self->message("save issue/$number"); open my $fh, ">$number.json"; print $fh $json->encode($issue); close $fh; if (exists $issue->{pull_request}{patch_url}) { $ua->mirror($issue->{pull_request}{patch_url} => "$number.patch"); } } return $self; } sub backup { my $self = shift; $self->clone_git if ($self->has_downloads eq 'true'); $self->set_forks if ($self->forks_count > 0); $self->clone_wiki if ($self->has_wiki eq 'true'); $self->save_issues if ($self->has_issues eq 'true'); return $self; } 1; __END__