RSS Git Download  Clone
Raw Blame History
#===============================================================================
#
#  DESCRIPTION: plugin for LIMS::Local::IssueTracker
#  requires two gitlab boards. 'RFCs' and 'Test' both containing 'Backlog' and
#  'Completed' lists
#
#===============================================================================
package LIMS::Local::IssueTracker::Plugin::GitLab;

our $VERSION = "0.01";
use Modern::Perl;
use utf8;
use Carp;

use GitLab::API::v4;

use Moose::Role;
use namespace::autoclean;
use Data::Printer;
use JSON;
use Try::Tiny;

has 'tracker' => (
    is      => 'ro',
    isa     => 'GitLab::API::v4',
    lazy    => '1',
    builder => '_build_gitlab'
);
has 'project' => (
    is      => 'ro',
    isa     => 'Int',
    lazy    => 1,
    builder => '_build_project'
);
with 'LIMS::Local::Role::Trackable';

sub _build_gitlab {
    my $self  = shift;
#    my $key   = $self->config->{key};
    my $url           = $self->config->{url};
    my $private_token = $self->config->{private_token};

    my $t = GitLab::API::v4->new(url => $url, private_token => $private_token);
#    my $t = GitLab::API::v4->new(url => 'https://gitlab.com', private_token => 'GGDQo53SAL4FbMFcu1mb');
    return $t;
}
# set project id depending on whether we are testing
# use gitlab api and config file to determine
sub _build_project {
    my $self = shift;
    my $project_id; # gitlabs internal id for project
    if ( not $ENV{DEBUG_GITLAB_LIVE} && ($ENV{HARNESS_ACTIVE} || $ENV{TESTRUN} )) {
        my $p = $self->tracker->user_projects($self->config->{board}->{user});
        ($project_id) = map {$_->{id}}grep {$_->{name} =~ /Testproject/}  @$p;
    }
    else {
        my $p = $self->tracker->group_projects($self->config->{board}->{group});
        ($project_id) = map {$_->{id}}grep {$_->{name} =~ /HILIS4/}  @$p;
    }
    return $project_id;
}

sub check_config {
    my $self   = shift;
    my $config = $self->config;

    # mandatory config fields
    my @mandatory_issue_lists = qw/new complete rejected/;

    if (
        ( grep    { not exists $config->{$_} } qw(url private_token) )
        or ( grep { not exists $config->{board}->{lists}->{$_}->{name} }
            @mandatory_issue_lists )
        or ( grep { not exists $config->{board}->{lists}->{$_}->{id} }
            @mandatory_issue_lists )
      )
    {
        return 0;
    }
    if ( not $ENV{DEBUG_GITLAB_LIVE} && ($ENV{HARNESS_ACTIVE} || $ENV{TESTRUN} )) {
        if ( not exists $config->{board}->{user} ) {
            return 0;
        }
    }
    else {                                            # not testing
        if ( not exists $config->{board}->{group} ) {
            return 0;
        }
    }
    return 1;
}

sub create_issue {
    my $self           = shift;
    my $args           = shift;
    my $tracker        = $self->tracker;
    my $new_cards_list = $self->config->{board}->{lists}->{new}->{name};
    my $res;
    # first check the colour is valid
    my $colour  = $self->config->{colours}->{ $args->{reason} };
    croak "Tracker error adding label to card: $!" unless $colour;

    # now make the issue
    try {
        $res = $tracker->create_issue(
            $self->project,
            {
                #labels      => [$new_cards_list, $colour],
                labels      => [$new_cards_list, $args->{reason}],
                title       => $args->{name},
                description => $args->{desc}
            }
        );
    }
    catch { croak "Tracker error posting new card: $_"; };


    return $res->{iid};

}

# returns raw status from trello (not the config list names)
sub get_status {
    my $self = shift;
    my $id   = shift;
    croak "no id" unless $id;
    my $tracker = $self->tracker;
    my $res    = try { $tracker->issue($self->project, $id) } catch {
    croak "Tracker error: can't get card $id :$_"};


    if (grep {/rejected/} @{$res->{labels}}){
        return 'rejected';
    }
    else {
        return $res->{state};
    }

}

# convert back to generic list name from config
sub get_status_config_name {
    my $self    = shift;
    my $card_id = shift;
    my $list    = $self->get_status($card_id);
    my ($key) =
      grep { $self->config->{board}->{lists}->{$_}->{name} eq $list }
      keys $self->config->{board}->{lists};

    return $key;

}

# have we finished with it?
sub is_complete_or_rejected {
    my $self = shift;
    my $id   = shift;

    my @list_names = qw/complete rejected/;
    my $status = grep { $self->is_status( $_, $id ) } @list_names;
    if ($status) {
        return 1;
    }
    else { return 0; }
}

# boolean check if card is in list
sub is_status {
    my $self      = shift;
    my $list_name = shift;
    my $id        = shift;
    my $list      = $self->config->{board}->{lists}->{$list_name}->{name};
    my $status    = $self->get_status($id);
    if ( $status eq $list ) {
        return 1;
    }
    else { return 0; }
}

# takes ArrayRef of ids and returns a ArrayRef of ids in Completed list
sub list_complete {
    my $self     = shift;
    my $ids      = shift;
    my @complete = grep { $self->is_status( 'complete', $_ ) } @$ids;
    return \@complete;
}

# takes ArrayRef of ids and returns a ArrayRef of ids in Rejected list
sub list_rejected {
    my $self     = shift;
    my $ids      = shift;
    my @rejected = grep { $self->is_status( 'rejected', $_ ) } @$ids;
    return \@rejected;
}

sub move_card {
    my $self        = shift;
    my $tracker      = $self->tracker;
    my $card_id     = shift;
    my $destination = shift || croak "requires destination: $!";
    try {
        $tracker->edit_issue( $self->project, $card_id,
            { state_event => $self->config->{board}->{lists}->{$destination}->{id} } );
        if ( $destination eq "rejected" ) { # just a label on the closed board issues
            my @labels =  @{ $tracker->issue($self->project, $card_id)->{labels} };
            push  @labels, $destination;
            $tracker->edit_issue( $self->project, $card_id,
#                { labels => [ $self->config->{board}->{lists}->{$destination}->{name} ] }
                { labels => \@labels }
            );

        }
    } catch {
    croak "Tracker error: can't move card $card_id :$_";

    }
}

# change status of card from closed: True to closed:False
# needed for #RfC::recreate_issue
sub unarchive_card {
    croak "No archive concept in GitLab";
    my $self    = shift;
    my $tracker  = $self->trello;
    my $card_id = shift;
    my $res     = $tracker->put( "cards/$card_id", { closed => 'false' } );
    croak "Trello error: can't unarchive card $card_id :$!"
      if $res->code != 200;
}

# archive_issues archives all cards  from a given list
# @ARGS "listname" looks up list name from config and uses that id in api
sub archive_issues {
    croak "No archive concept in GitLab";
    my $self    = shift;
    my $tracker  = $self->trello;
    my $list    = shift || croak "requires list name $!";
    my $list_id = $self->config->{board}->{lists}->{$list}->{id};

    my $res = $tracker->post("lists/$list_id/ArchiveAllCards");
    croak "Trello error: can't archive cards from list $list :$!"
      if $res->code != 200;
}

#returns a list of iids of all issues
sub all_iids {
    my $self    = shift;
    my $tracker = $self->tracker;

    my $issues = $tracker->issues($self->project);
    my @iids = map {$_->{iid}} @$issues;
    return @iids;
}

# delete issues as a clean up operation for testing
# only works in test env for safety
sub delete_all_issues {
    my $self    = shift;
    my $tracker = $self->tracker;

    if ( $ENV{HARNESS_ACTIVE} || $ENV{TESTRUN} ) {
        my $issues = $tracker->issues($self->project);
        my @iids = map {$_->{iid}} @$issues;
        for my $id (@iids) {
            $tracker->delete_issue($self->project, $id);
        }
    }
    else {
        croak "Running delete_all_issues() outside of a test is dangerous";

    }
}

1;

=head1 NAME

LIMS::Local::IssueTracker::Plugin::GitLab

=head1 VERSION

$VERSION

=head1 SYNOPSIS

use LIMS::Local::IssueTracker;

$self->load_plugin( LIMS::Local::IssueTracker::Plugin::GitLab );
croak "not trackable" unless $self->does("LIMS::Local::Role::Trackable");

=head1 DESCRIPTION

Plugin for LIMS::Local::IssueTracker. Implements LIMS::Local::Role::Trackable


=head1 DIAGNOSTICS

=over 12

=item C<all_iids>

retrieves list of ids from issues board
returns array

=item C<archive_issues>

Disabled

removes cards from list
param Str listname (from yaml)
TODO wont work in GitLab as not worked out archiving yet
maybe can just label as archived and have a hidden list for archived closed issues

=item C<check_config>

validates config is usable

=item C<create_issue>

adds a card to the new GitLab list
params: { {name} , {desc} }

=item C<delete_all_issues>

deletes all issues
param none
Note: only in testmode

=item C<get_status>

returns raw list name from issue tracker

=item C<get_status_config_name>

returns config mapped list name if mapping exists

=item C<is_status>

boolean check if card is in list


=item C<list_complete>

takes ArrayRef of ids and returns a ArrayRef of ids in complete list but not rejected


=item C<list_rejected>

takes ArrayRef of ids and returns a ArrayRef of ids labelled as rejected


=item C<move_card>

transfer cards between lists

params cardid, listname
listname is from config list names

can only transfer to valid lists from config (no long_grass)

=item C<unarchive_card>

Disabled

reinstate card that has been archived

=back

=head1 CONFIGURATION AND ENVIRONMENT

Yaml configuration


---
plugin: GitLab
url: https://gitlab.com/api/v4
private_token: <FROM GITLAB>
# irrelevant in gitlab
colours:
  new feature: green
  modification: yellow
  fix error: red
  change menu: blue
board:
  live:
    name: HILIS4
    group: hmds_software
    id:
    lists:
      new:
        name: rfc
        id:
      complete:
        name: close # need to change status instead
        id:
      rejected:
        name: rejected
        id:
  test:
    user: hmds.lth
    name: Testproject
    id:
    lists:
      new:
        name: rfc
        id: reopen
      complete:
        name: closed
        id: close
      rejected:
        name: rejected
        id: close



=head1 DEPENDENCIES

    GitLab::API::v4
    Try::Tiny
    Moose::Role


=head1 INCOMPATIBILITIES



=head1 BUGS AND LIMITATIONS

GitLab Issues have no concept of archiving so the archive functions croak for now
In the future they may be implemented so they are deleted from GitLab but recovered via the database if that is useful


=head1 AUTHOR

Garry Quested (garry.quested@nhs.net)

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2019 Garry Quested (garry.quested@nhs.net). All rights reserved.

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See L<perlartistic>.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


=cut