API-GitForge

 view release on metacpan or  search on metacpan

lib/API/GitForge.pm  view on Meta::CPAN

use API::GitForge::GitLab;

our @EXPORT_OK = qw(new_from_domain forge_access_token remote_forge_info);

our %known_forges = (
    "github.com"       => "API::GitForge::GitHub",
    "salsa.debian.org" => "API::GitForge::GitLab",
);


sub new_from_domain {
    my %opts = @_;
    croak "unknown domain" unless exists $known_forges{ $opts{domain} };
    $known_forges{ $opts{domain} }->new(%opts);
}


sub forge_access_token {
    my $domain = shift;
    my $root = $ENV{XDG_CONFIG_HOME} || catfile $ENV{HOME}, ".config";
    my $file = catfile $root, "gitforge", "access_tokens", $domain;
    -e $file and -r _ or croak "$file does not exist or is not readable";
    open my $fh, "<", $file or die "failed to open $file for reading: $!";
    chomp(my $key = <$fh>);
    $key;
}


sub remote_forge_info {
    my $remote = shift;
    my $git    = Git::Wrapper->new(getcwd);
    my ($uri) = $git->remote("get-url", $remote);
    $uri =~ m#^https?://([^:/@]+)/#
      or $uri =~ m#^(?:\w+\@)?([^:/@]+):#
      or croak "couldn't determine git forge info from $remote remote";
    ($1, $');
}

1;

lib/API/GitForge/GitHub.pm  view on Meta::CPAN

use strict;
use warnings;

use Role::Tiny::With;

use Carp;
use Net::GitHub;

with "API::GitForge::Role::GitForge";

sub _make_api {
    my $self = shift;
    my %opts;
    $opts{access_token} = $self->{_access_token}
      if exists $self->{_access_token};
    $self->{_api} = Net::GitHub->new(%opts);
}

sub _ensure_fork {
    my ($self, $upstream) = @_;
    my ($org,  $repo)     = _extract_repo($upstream);

    my $repos       = $self->{_api}->repos;
    my $user        = $self->{_api}->user->show->{login};
    my @user_repos  = $repos->list_user($user);
    my $repo_exists = sub {
        grep { $_->{name} eq $repo } @user_repos;
    };
    if (&$repo_exists) {
        $self->_assert_fork_has_parent($upstream);
    } else {
        $repos->create_fork($org, $repo);
        until (&$repo_exists) {
            sleep 5;
            @user_repos = $repos->list_user($user);
        }
    }
    return "https://github.com/$user/$repo";
}

sub _assert_fork_has_parent {
    my ($self, $upstream) = @_;
    my (undef, $repo)     = _extract_repo($upstream);
    my $user = $self->{_api}->user->show->{login};
    my $fork = $self->{_api}->repos->get($user, $repo);

    $fork->{parent}{full_name} eq $upstream
      or croak
      "$user/$repo does not have parent $upstream; don't know what to do";
}

sub _clean_config_repo {
    my ($self, $target) = @_;
    my ($org,  $repo)   = _extract_repo($target);
    my $repos = $self->{_api}->repos;
    $repos->set_default_user_repo($org, $repo);
    $repos->update({
        name          => "$repo",
        has_wiki      => 0,
        has_issues    => 0,
        has_downloads => 0,
        has_pages     => 0,
        has_projects  => 0,
    });
}

sub _clean_config_fork {
    my ($self, $upstream) = @_;
    my (undef, $repo)     = _extract_repo($upstream);
    my $user = $self->{_api}->user->show->{login};

    my $repos = $self->{_api}->repos;
    $repos->set_default_user_repo($user, $repo);
    $repos->update({
        name           => "$repo",
        homepage       => "",
        description    => "Temporary fork for pull request(s)",
        default_branch => "gitforge",
    });

    $self->_clean_config_repo("$user/$repo");
}

sub _ensure_repo {
    my ($self, $target) = @_;
    my ($org,  $repo)   = _extract_repo($target);
    my $repos       = $self->{_api}->repos;
    my $user        = $self->{_api}->user->show->{login};
    my %create_opts = (name => $repo);
    my $list_method;
    if ($org eq $user) {
        $list_method = "list_user";
    } else {
        $list_method = "list_org";
        $create_opts{org} = $org unless $org eq $user;
    }
    my @list_repos  = $repos->$list_method($org);
    my $repo_exists = sub {
        grep { $_->{name} eq $repo } @list_repos;
    };
    unless (&$repo_exists) {
        $repos->create(\%create_opts);
        until (&$repo_exists) {
            sleep 5;
            @list_repos = $repos->$list_method($org);
        }
    }
    return "https://github.com/$org/$repo";
}

sub _nuke_fork {
    my ($self, $upstream) = @_;
    $self->_assert_fork_has_parent($upstream);
    my (undef, $repo)     = _extract_repo($upstream);
    my $user = $self->{_api}->user->show->{login};
    $self->{_api}->repos->delete($user, $repo);
}

sub _extract_repo {
    $_[0] =~ m#^([^/]+)/(.+)(?:\.git)?$#;
    ($1, $2);
}

1;

__END__

=pod

lib/API/GitForge/GitLab.pm  view on Meta::CPAN

use strict;
use warnings;

use Role::Tiny::With;

use Carp;
use GitLab::API::v4;

with "API::GitForge::Role::GitForge";

sub _make_api {
    my $self = shift;
    my %opts = (url => "https://" . $self->{_domain} . "/api/v4");
    $opts{private_token} = $self->{_access_token}
      if exists $self->{_access_token};
    $self->{_api} = GitLab::API::v4->new(%opts);
}

sub _ensure_fork {
    my ($self, $upstream) = @_;
    my ($path, $repo)     = _extract_project_id($upstream);

    my $user = $self->{_api}->current_user->{username};
    my @user_repos;
    my $update_user_repos = sub {
        @user_repos
          = @{ $self->{_api}->projects({ search => "$user/$repo" }) };
    };
    my $repo_exists = sub {
        grep { $_->{path_with_namespace} eq "$user/$repo" } @user_repos;
    };
    &$update_user_repos;
    if (&$repo_exists) {
        $self->_assert_fork_has_parent($upstream);
    } else {
        $self->{_api}->fork_project("$path/$repo");
        until (&$repo_exists) {
            sleep 5;
            &$update_user_repos;
        }
    }
    return "https://" . $self->{_domain} . "/$user/$repo.git";
}

sub _assert_fork_has_parent {
    my ($self, $upstream) = @_;
    my ($path, $repo)     = _extract_project_id($upstream);
    my $user = $self->{_api}->current_user->{username};
    my $fork = $self->{_api}->project("$user/$repo");

    $fork->{forked_from_project}{path_with_namespace} eq $path . "/" . $repo
      or croak
      "$user/$repo does not have parent $upstream; don't know what to do";
}

sub _clean_config_repo {
    my ($self, $target) = @_;
    my ($ns,   $repo)   = _extract_project_id($target);

    $self->{_api}->edit_project(
        "$ns/$repo",
        {
            issues_access_level         => "disabled",
            merge_requests_access_level => "disabled",
        });
}

sub _clean_config_fork {
    my ($self, $upstream) = @_;
    my (undef, $repo)     = _extract_project_id($upstream);
    my $user = $self->{_api}->current_user->{username};

    $self->{_api}->edit_project(
        "$user/$repo",
        {
            default_branch      => "gitforge",
            description         => "Temporary fork for merge request(s)",
            issues_access_level => "disabled",
            # merge requests have to be enabled in the fork in order
            # to submit merge requests to the upstream repo from which
            # we forked, it seems
            merge_requests_access_level => "enabled",
        });
}

sub _ensure_repo {
    my ($self, $target) = @_;
    my ($ns,   $repo)   = _extract_project_id($target);
    return if $self->{_api}->project($target);
    my $namespace = $self->{_api}->namespace($ns)
      or croak "invalid project namespace $ns";
    $self->{_api}
      ->create_project({ name => $repo, namespace_id => $namespace->{id} });
}

sub _nuke_fork {
    my ($self, $upstream) = @_;
    $self->_assert_fork_has_parent($upstream);
    my (undef, $repo) = _extract_project_id($upstream);
    my $user = $self->{_api}->current_user->{username};
    $self->{_api}->delete_project("$user/$repo");
}

sub _ensure_fork_branch_unprotected {
    my ($self, $upstream, $branch) = @_;
    my (undef, $repo) = _extract_project_id($upstream);
    my $user = $self->{_api}->current_user->{username};
    return unless $self->{_api}->protected_branch("$user/$repo", $branch);
    $self->{_api}->unprotect_branch("$user/$repo", $branch);
}

sub _extract_project_id {
    my $project = shift;
    $project =~ s#(?:\.git)?/?$##;
    $project =~ m#/([^/]+)$#;
    ($`, $1);
}

1;

__END__

lib/API/GitForge/Role/GitForge.pm  view on Meta::CPAN

use warnings;

use Role::Tiny;

use Carp;
use File::Temp qw(tempdir);
use Git::Wrapper;
use File::Spec::Functions qw(catfile);


sub new {
    my ($class, %opts) = @_;
    croak "need domain!" unless exists $opts{domain};

    my %attrs = (_domain => $opts{domain});
    $attrs{_access_token} = $opts{access_token} if exists $opts{access_token};
    my $self = bless \%attrs => $class;

    $self->_make_api;

    return $self;
}


sub ensure_repo { shift->_create_repo(@_) }


sub clean_repo {
    my ($self, $repo) = @_;
    $self->_ensure_repo($repo);
    $self->_clean_config_repo($repo);
}


sub ensure_fork { shift->_ensure_fork(@_) }


sub clean_fork {
    my $self     = shift;
    my $fork_uri = $self->_ensure_fork($_[0]);

    my $temp = tempdir CLEANUP => 1;
    my $git = Git::Wrapper->new($temp);
    $git->init;
    my @fork_branches
      = map { m#refs/heads/#; $' } $git->ls_remote("--heads", $fork_uri);
    return $fork_uri if grep /\Agitforge\z/, @fork_branches;

lib/API/GitForge/Role/GitForge.pm  view on Meta::CPAN

    if ($self->can("_ensure_fork_branch_unprotected")) {
        $self->_ensure_fork_branch_unprotected($_[0], $_) for @fork_branches;
    }
    # may fail if we couldn't unprotect; that's okay
    eval { $git->push($fork_uri, "--delete", @fork_branches) };

    return $fork_uri;
}


sub nuke_fork { shift->_nuke_fork(@_) }


sub clean_config_repo { shift->_clean_config_repo(@_) }


sub clean_config_fork { shift->_clean_config_fork(@_) }

requires
  qw<_make_api _ensure_repo _clean_config_repo _clean_config_fork
     _ensure_fork _nuke_fork>;

1;

__END__

=pod

lib/App/git/clean_forge_fork.pm  view on Meta::CPAN

use Git::Wrapper;
use API::GitForge qw(new_from_domain forge_access_token remote_forge_info);
use Try::Tiny;
use Cwd;

my $exit_main = 0;

CORE::exit main unless caller;


sub main {
    shift if $_[0] and ref $_[0] eq "";
    local @ARGV = @{ $_[0] } if $_[0] and ref $_[0] ne "";

    my $term     = Term::ReadLine->new("brand");
    my $upstream = "origin";
    my $git      = Git::Wrapper->new(getcwd);
    #<<<
    try {
        $git->rev_parse({ git_dir => 1 });
    } catch {

lib/App/git/clean_forge_fork.pm  view on Meta::CPAN

        $fork_uri eq ($git->remote(qw(get-url fork)))[0]
          or die "fork remote exists but has wrong URI\n";
    } else {
        $git->remote(qw(add fork), $fork_uri);
    }

  EXIT_MAIN:
    return $exit_main;
}

sub exit { $exit_main = shift // 0; goto EXIT_MAIN }

__END__

=pod

=encoding UTF-8

=head1 NAME

App::git::clean_forge_fork - create tidy forks for pull requests

lib/App/git/clean_forge_repo.pm  view on Meta::CPAN

use Getopt::Long;
use Git::Wrapper;
use API::GitForge qw(new_from_domain forge_access_token remote_forge_info);
use Try::Tiny;

my $exit_main = 0;

CORE::exit main unless caller;


sub main {
    shift if $_[0] and ref $_[0] eq "";
    local @ARGV = @{ $_[0] } if $_[0] and ref $_[0] ne "";

    my $term   = Term::ReadLine->new("brand");
    my $remote = "origin";
    my $git    = Git::Wrapper->new(getcwd);
    #<<<
    try {
        $git->rev_parse({ git_dir => 1 });
    } catch {

lib/App/git/clean_forge_repo.pm  view on Meta::CPAN


    my $forge = new_from_domain
      domain       => $forge_domain,
      access_token => forge_access_token $forge_domain;
    $forge->clean_repo($forge_repo);

  EXIT_MAIN:
    return $exit_main;
}

sub exit { $exit_main = shift // 0; goto EXIT_MAIN }

__END__

=pod

=encoding UTF-8

=head1 NAME

App::git::clean_forge_repo - create repos on git forges with optional features disabled

lib/App/git/nuke_forge_fork.pm  view on Meta::CPAN

use API::GitForge qw(new_from_domain forge_access_token remote_forge_info);
use Try::Tiny;
use Cwd;
use Term::UI;

my $exit_main = 0;

CORE::exit main unless caller;


sub main {
    shift if $_[0] and ref $_[0] eq "";
    local @ARGV = @{ $_[0] } if $_[0] and ref $_[0] ne "";

    my $term     = Term::ReadLine->new("brand");
    my $upstream = "origin";
    my $git      = Git::Wrapper->new(getcwd);
    #<<<
    try {
        $git->rev_parse({ git_dir => 1 });
    } catch {

lib/App/git/nuke_forge_fork.pm  view on Meta::CPAN

    my $forge = new_from_domain
      domain       => $forge_domain,
      access_token => forge_access_token $forge_domain;
    $forge->nuke_fork($upstream_repo);
    $git->remote(qw(rm fork));

  EXIT_MAIN:
    return $exit_main;
}

sub exit { $exit_main = shift // 0; goto EXIT_MAIN }

__END__

=pod

=encoding UTF-8

=head1 NAME

App::git::nuke_forge_fork - delete forks created by git-clean-forge-fork(1)



( run in 0.494 second using v1.01-cache-2.11-cpan-4d50c553e7e )