2018-07-04 10:46:13 +02:00
|
|
|
package Git::IssueManager::Issue;
|
|
|
|
#ABSTRACT: class representing an Issue
|
|
|
|
use Moose;
|
|
|
|
use File::Basename;
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
|
|
|
B<Git::IssueManager::Issue> represents an issue within the Git::IssueManager module. Issues can be
|
|
|
|
added, removed, modified and listed.
|
|
|
|
|
|
|
|
Make sure that you understand all the attributes before adding issues to your repository.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
|
|
|
|
=attr subject
|
|
|
|
|
|
|
|
The subject/ title of the issue
|
|
|
|
|
|
|
|
At most 50 chars allowed.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'subject' => (is => 'rw', isa => 'Str', required => 1, trigger => sub {
|
|
|
|
my ($self, $new, $old) = @_;
|
|
|
|
|
|
|
|
die("subject exceeds 50 chars") unless length($new) < 51;
|
|
|
|
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
=attr priority
|
|
|
|
|
|
|
|
The priority of the issue. Possible values are:
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
2018-09-15 22:48:07 +02:00
|
|
|
=item I<urgent> - the most highest level of priority
|
2018-07-04 10:46:13 +02:00
|
|
|
|
|
|
|
=item I<high>
|
|
|
|
|
|
|
|
=item I<medium>
|
|
|
|
|
|
|
|
=item I<low>
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
The default value is B<low>.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'priority' => (is => 'rw', isa => 'Str', default => 'low', trigger => sub {
|
|
|
|
my ($self, $new, $old) = @_;
|
|
|
|
|
|
|
|
die("unknown value (" . $new . ")") unless lc($new) eq "urgent" || lc($new) eq "high" ||
|
|
|
|
lc($new) eq "medium" || lc($new) eq "low";
|
|
|
|
});
|
|
|
|
|
|
|
|
=attr severity
|
|
|
|
|
|
|
|
The severity of the issue. Possible values are:
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item I<critical>
|
|
|
|
|
|
|
|
=item I<high>
|
|
|
|
|
|
|
|
=item I<medium>
|
|
|
|
|
|
|
|
=item I<low>
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
The default value is B<low>
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'severity' => (is => 'rw', isa => 'Str', default => 'low',trigger => sub {
|
|
|
|
my ($self, $new, $old) = @_;
|
|
|
|
|
|
|
|
die("unknown value (" . $new . ")") unless lc($new) eq "critical" || lc($new) eq "high" ||
|
|
|
|
lc($new) eq "medium" || lc($new) eq "low";
|
|
|
|
});
|
|
|
|
|
|
|
|
=attr type
|
|
|
|
|
|
|
|
The type of the issue. Possible values are:
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item I<bug> - a problem within the code, preventing the correct working of the software
|
|
|
|
|
|
|
|
=item I<security-bug> - a security related problem within the code, preventing the correct working of the software
|
|
|
|
|
|
|
|
=item I<improvement> - an enhancement to an already existing feature
|
|
|
|
|
|
|
|
=item I<feature> - a completly new feature
|
|
|
|
|
|
|
|
=item I<task> - a simple task, which should be done (please use rarely)
|
|
|
|
|
2018-09-15 22:28:22 +02:00
|
|
|
=item I<epic> - an epic , normally consisting out of multiple issues
|
|
|
|
|
2018-07-04 10:46:13 +02:00
|
|
|
=back
|
|
|
|
|
|
|
|
The default values is B<bug>.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'type' => (is => 'rw', isa => 'Str', default => 'bug', trigger => sub {
|
|
|
|
my ($self, $new, $old) = @_;
|
|
|
|
|
|
|
|
die("unknown value (" . $new . ")") unless lc($new) eq "bug" || lc($new) eq "security-bug" ||
|
|
|
|
lc($new) eq "improvement" || lc($new) eq "feature" ||
|
2018-09-15 22:28:22 +02:00
|
|
|
lc($new) eq "task" || lc($$new) eq "epic";
|
2018-07-04 10:46:13 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
=attr status
|
|
|
|
|
|
|
|
The status of the issue. Possible values are:
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item I<open> - nothing has been done yet
|
|
|
|
|
|
|
|
=item I<assigned> - the issue has been assigned to a developer
|
|
|
|
|
|
|
|
=item I<inprogess> - somebody is working on the issue
|
|
|
|
|
|
|
|
=item I<closed> - the issue is closed
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
The default value is B<open>.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'status' => (is => 'rw', isa => 'Str', default => 'open', trigger => sub {
|
|
|
|
my ($self, $new, $old) = @_;
|
|
|
|
|
|
|
|
die("unknown value (" . $new . ")") unless lc($new) eq "open" || lc($new) eq "assigned" ||
|
|
|
|
lc($new) eq "inprogress" || lc($new) eq "closed";
|
|
|
|
});
|
|
|
|
|
|
|
|
=attr substatus
|
|
|
|
|
|
|
|
A substatus to the actual status. Possible values are:
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item I<none> - there is no substatus
|
|
|
|
|
|
|
|
=item I<fixed> - the bug was fixed
|
|
|
|
|
|
|
|
=item I<wontfix> - the issue has been closed but it will never be fixed
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
The default value is B<none>.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'substatus' => (is => 'rw', isa => 'Str', default => 'none', trigger => sub {
|
|
|
|
my ($self, $new, $old) = @_;
|
|
|
|
|
|
|
|
die("unknown value (" . $new . ")") unless lc($new) eq "none" || lc($new) eq "fixed" ||
|
|
|
|
lc($new) eq "wontfix";
|
|
|
|
});
|
|
|
|
|
|
|
|
=attr comment
|
|
|
|
|
|
|
|
A comment to the current status of the issue.
|
|
|
|
|
|
|
|
Only Plain Text is allowed.
|
|
|
|
|
|
|
|
Default value is the empty string.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'comment' => (is => 'rw', isa=>'Str', default => "");
|
|
|
|
|
|
|
|
|
|
|
|
=attr description
|
|
|
|
|
|
|
|
The full description of the issue.
|
|
|
|
|
|
|
|
Only Plain Text and Markdown are allowed.
|
|
|
|
|
|
|
|
B<no HTML>
|
|
|
|
|
|
|
|
The default value is the empty string.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'description' => (is => 'rw', isa => 'Str', default => "");
|
|
|
|
|
|
|
|
=attr tags
|
|
|
|
|
|
|
|
An arrayref of tags/ keywords for better identifying the issue.
|
|
|
|
|
|
|
|
Maximum length of one tag is B<20> characters.
|
|
|
|
|
|
|
|
Maximum number of tags is B<10>.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'tags' => (is => 'rw', isa => 'ArrayRef[Str]', default => sub {return[];});
|
|
|
|
|
|
|
|
=attr attachments
|
|
|
|
|
|
|
|
An arrayref of files attached to this issue, for example documentation or text files presenting
|
|
|
|
error messages, screenshots, etc.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'attachements' => (is => 'rw', isa=> 'ArrayRef[Str]', default => sub{return [];});
|
|
|
|
|
|
|
|
|
|
|
|
=attr author
|
|
|
|
|
|
|
|
The author of the issue, can be the name or an anomynized nickname
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'author' => (is=> 'rw', isa => 'Str', default => "");
|
|
|
|
|
|
|
|
=attr author_email
|
|
|
|
|
|
|
|
The authors email for sending status changes of the issue
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'author_email' => (is => 'rw', isa => 'Str', default => "");
|
|
|
|
|
|
|
|
=attr worker
|
|
|
|
|
|
|
|
The persons name working on solving the issue
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'worker' => (is => 'rw', isa => 'Str', default => "");
|
|
|
|
|
|
|
|
=attr worker_email
|
|
|
|
|
|
|
|
The email address of the person working on this issue
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'worker_email' => ( is => 'rw', isa => 'Str', default =>"");
|
|
|
|
|
|
|
|
=attr creation_date
|
|
|
|
|
|
|
|
A datetime object representing the date/time the issue was created
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'creation_date' => (is => 'rw', isa=>'DateTime',default => sub{return DateTime->now();});
|
|
|
|
|
|
|
|
=attr closed_date
|
|
|
|
|
|
|
|
A datetime object representing the date/time the issue was closed, only valid if status is closed
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'closed_date' => (is => 'rw', isa=>'DateTime',default => sub{return DateTime->now();});
|
|
|
|
|
|
|
|
=attr last_change_date
|
|
|
|
|
|
|
|
A datetime object representing the date/time the issue was last modified
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'last_change_date' => (is => 'rw', isa=>'DateTime',default => sub{return DateTime->now();});
|
|
|
|
|
|
|
|
=attr id
|
|
|
|
|
|
|
|
id of the issue
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'id' => (is => 'rw', isa => 'Str', default => "");
|
|
|
|
|
2018-07-04 11:02:52 +02:00
|
|
|
=attr estimated_time
|
|
|
|
|
|
|
|
The estimated time for solving this issue in B<Minutes>
|
|
|
|
|
|
|
|
Default value is B<0>, meaning no estimate set.
|
|
|
|
|
|
|
|
=cut
|
2018-07-04 11:07:33 +02:00
|
|
|
has 'estimated_time' => (is => 'rw', isa => 'Num', default => 0, trigger => sub {
|
2018-07-04 11:02:52 +02:00
|
|
|
my ($self, $new, $old) = @_;
|
|
|
|
|
2018-07-04 16:16:35 +02:00
|
|
|
die("unknown value (" . $new . ")") unless $new >=0 ;
|
2018-07-04 11:02:52 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
=attr working_time
|
|
|
|
|
|
|
|
The current time in B<Minutes> already spent on this issue
|
|
|
|
|
|
|
|
The default value is B<0>.
|
|
|
|
|
|
|
|
=cut
|
2018-07-04 11:07:33 +02:00
|
|
|
has 'working_time' => (is => 'rw', isa=>'Num', default => 0, trigger => sub {
|
2018-07-04 11:02:52 +02:00
|
|
|
my ($self, $new, $old) = @_;
|
|
|
|
|
2018-07-04 16:16:35 +02:00
|
|
|
die("unknown value (" . $new . ")") unless $new >= 0;
|
2018-07-04 11:02:52 +02:00
|
|
|
});
|
|
|
|
|
2018-07-04 10:46:13 +02:00
|
|
|
|
2018-09-15 22:46:47 +02:00
|
|
|
=attr close_commit
|
|
|
|
|
|
|
|
the hash of the commit which closed this issue
|
|
|
|
|
|
|
|
=cut
|
|
|
|
has 'close_commit' => (is => 'rw', isa=>'Str', default =>"");
|
|
|
|
|
|
|
|
|
2018-07-04 10:46:13 +02:00
|
|
|
=method addTag
|
|
|
|
|
|
|
|
add another tag to the issue.
|
|
|
|
|
|
|
|
B<Example:>
|
|
|
|
|
|
|
|
$issue->addTag("File");
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item B<1. Parameter:> Tag to add to the issue
|
|
|
|
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub addTag
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
my $tag = shift;
|
|
|
|
|
|
|
|
die("no tag given") unless defined($tag);
|
|
|
|
die("too many tags") unless @{$self->tags}<11;
|
|
|
|
die("tag exceeds 20 chars") unless length($tag) < 21;
|
|
|
|
|
|
|
|
push (@{$self->tags}, $tag);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
=method delTag
|
|
|
|
|
|
|
|
del a tag from the issue
|
|
|
|
|
|
|
|
B<Example:>
|
|
|
|
|
|
|
|
$issue->delTag("File");
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item B<1. Parameter> Tag to remove from issue
|
|
|
|
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub delTag
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
my $tag = shift;
|
|
|
|
|
|
|
|
die("no tag given") unless defined($tag);
|
|
|
|
|
|
|
|
my $i = 0;
|
|
|
|
|
|
|
|
for my $t (@{$self->tags})
|
|
|
|
{
|
|
|
|
if ($t eq $tag)
|
|
|
|
{
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
$i++;
|
|
|
|
}
|
|
|
|
|
|
|
|
splice(@{$self->tags},$i,1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
=method addAttachment
|
|
|
|
|
|
|
|
Add another attachment to the issue.
|
|
|
|
|
|
|
|
B<Example:>
|
|
|
|
|
|
|
|
$issue->addAttachement("/tmp/test.txt");
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item B<1. Parameter> path to the attachment to add
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
Make sure the attachment exist at the given path and stays there until the issue has been
|
|
|
|
added.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
sub addAttachement
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
my $attachment = shift;
|
|
|
|
|
|
|
|
die("no attachment given") unless defined($attachment);
|
|
|
|
die("file does not exist") unless -e $attachment;
|
|
|
|
|
|
|
|
|
|
|
|
push (@{$self->attachements}, $attachment);
|
|
|
|
}
|
|
|
|
|
|
|
|
=method delAttachement
|
|
|
|
|
|
|
|
Remove an attachment from the issue.
|
|
|
|
|
|
|
|
B<Example:>
|
|
|
|
|
|
|
|
$issue->delAttachement("/tmp/test");
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item B<1. Parameter> Attachment path to remove from issue
|
|
|
|
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub delAttachment
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
my $attachment = shift;
|
|
|
|
|
|
|
|
die("no attachment given") unless defined($attachment);
|
|
|
|
|
|
|
|
my $i = 0;
|
|
|
|
|
|
|
|
for my $a (@{$self->attachments})
|
|
|
|
{
|
|
|
|
if ($a eq $attachment)
|
|
|
|
{
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
$i++;
|
|
|
|
}
|
|
|
|
|
|
|
|
splice(@{$self->attachments},$i,1);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
=method _createAttachmentTree - internal method, do not call directly
|
|
|
|
|
|
|
|
creates a git repository tree object from the attachment array and return the hash of the object
|
|
|
|
|
|
|
|
=over
|
|
|
|
=item B<1. Parameter> reference to a Git::RepositoryHL object
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
sub _createAttachmentTree
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
my $repository = shift;
|
|
|
|
my @tree;
|
|
|
|
|
|
|
|
for my $a (@{$self->attachments})
|
|
|
|
{
|
|
|
|
my $hash = $repository->createFileObjectFromFile($a);
|
|
|
|
my $t = {
|
|
|
|
ref => $hash,
|
|
|
|
path => fileparse($a),
|
|
|
|
mode => "100644",
|
|
|
|
type => "blob"
|
|
|
|
};
|
|
|
|
push(@tree, $t);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $repository->createTree(\@tree);
|
|
|
|
}
|
|
|
|
|
|
|
|
=method createIssue
|
|
|
|
|
|
|
|
Creates the issue inside the given git repository and commits these changes to the issues branch
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item B<1. Parameter> reference to a Git::RepositoryHL object
|
|
|
|
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub createIssue
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
my $repository = shift;
|
|
|
|
|
2018-09-10 22:41:23 +02:00
|
|
|
die("No Git::LowLevel object given") unless ref($repository) eq "Git::LowLevel";
|
|
|
|
|
|
|
|
my $ref = $repository->getReference('refs/heads/issues');
|
|
|
|
my $root = $ref->getTree();
|
|
|
|
|
|
|
|
my $issueTree = $root->newTree();
|
|
|
|
my $path = $self->subject;
|
|
|
|
$path=~s/\s/_/g;
|
|
|
|
$issueTree->path($self->subject);
|
|
|
|
|
|
|
|
my $subject = $issueTree->newBlob();
|
|
|
|
$subject->path("subject");
|
|
|
|
$subject->_content($self->subject);
|
|
|
|
$issueTree->add($subject);
|
|
|
|
|
|
|
|
my $priority = $issueTree->newBlob();
|
|
|
|
$priority->path("priority");
|
2018-09-16 11:21:24 +02:00
|
|
|
$priority->_content(lc($self->priority));
|
2018-09-10 22:41:23 +02:00
|
|
|
$issueTree->add($priority);
|
|
|
|
|
|
|
|
my $severity = $issueTree->newBlob();
|
|
|
|
$severity->path("severity");
|
2018-09-16 11:21:24 +02:00
|
|
|
$severity->_content(lc($self->severity));
|
2018-09-10 22:41:23 +02:00
|
|
|
$issueTree->add($severity);
|
|
|
|
|
|
|
|
my $type = $issueTree->newBlob();
|
|
|
|
$type->path("type");
|
2018-09-16 11:21:24 +02:00
|
|
|
$type->_content(lc($self->type));
|
2018-09-10 22:41:23 +02:00
|
|
|
$issueTree->add($type);
|
|
|
|
|
2018-09-15 22:24:05 +02:00
|
|
|
my $last_changed = $issueTree->newBlob();
|
|
|
|
$last_changed->path("last_change_date");
|
|
|
|
$last_changed->_content($self->last_change_date->epoch());
|
|
|
|
$issueTree->add($last_changed);
|
|
|
|
|
|
|
|
my $creation_date = $issueTree->newBlob();
|
|
|
|
$creation_date->path("creation_date");
|
|
|
|
$creation_date->_content($self->creation_date->epoch());
|
|
|
|
$issueTree->add($creation_date);
|
|
|
|
|
|
|
|
if ($self->status eq "closed")
|
|
|
|
{
|
|
|
|
my $closed_date = $issueTree->newBlob();
|
|
|
|
$closed_date->path("closed_date");
|
|
|
|
$closed_date->_content($self->closed_date->epoch());
|
|
|
|
$issueTree->add($closed_date);
|
|
|
|
}
|
|
|
|
|
2018-09-10 22:41:23 +02:00
|
|
|
if (defined($self->substatus) && length($self->substatus) > 0)
|
2018-07-04 10:46:13 +02:00
|
|
|
{
|
2018-09-10 22:41:23 +02:00
|
|
|
my $substatus = $issueTree->newBlob();
|
|
|
|
$substatus->path("substatus");
|
|
|
|
$substatus->_content($self->substatus);
|
|
|
|
$issueTree->add($substatus);
|
2018-07-04 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
2018-09-10 22:41:23 +02:00
|
|
|
if (defined($self->comment) && length($self->comment)> 0)
|
|
|
|
{
|
|
|
|
my $comment = $issueTree->newBlob();
|
|
|
|
$comment->path("comment");
|
|
|
|
$comment->_content($self->comment);
|
|
|
|
$issueTree->add($comment);
|
|
|
|
}
|
|
|
|
|
2018-09-15 22:46:47 +02:00
|
|
|
if (defined($self->close_commit) && length($self->close_commit)> 0 && $self->status eq "closed")
|
|
|
|
{
|
|
|
|
my $close_commit = $issueTree->newBlob();
|
|
|
|
$close_commit->path("close_commit");
|
|
|
|
$close_commit->_content($self->close_commit);
|
|
|
|
$issueTree->add($close_commit);
|
|
|
|
}
|
|
|
|
|
2018-09-10 22:41:23 +02:00
|
|
|
if (defined($self->description) && length($self->description)> 0)
|
|
|
|
{
|
|
|
|
my $description = $issueTree->newBlob();
|
|
|
|
$description->path("description");
|
|
|
|
$description->_content($self->description);
|
|
|
|
$issueTree->add($description);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (defined($self->worker) && length($self->worker)> 0)
|
|
|
|
{
|
|
|
|
my $worker = $issueTree->newBlob();
|
|
|
|
$worker->path("worker");
|
|
|
|
$worker->_content($self->worker . "<" . $self->worker_email . ">");
|
|
|
|
$issueTree->add($worker);
|
|
|
|
}
|
|
|
|
|
|
|
|
my $estimated = $issueTree->newBlob();
|
|
|
|
$estimated->path("estimated");
|
|
|
|
$estimated->_content($self->estimated_time);
|
|
|
|
$issueTree->add($estimated);
|
|
|
|
|
|
|
|
my $working_time = $issueTree->newBlob();
|
|
|
|
$working_time->path("working");
|
|
|
|
$working_time->_content($self->working_time);
|
|
|
|
$issueTree->add($working_time);
|
|
|
|
|
|
|
|
if (defined($self->tags) && @{$self->tags}> 0)
|
|
|
|
{
|
|
|
|
my $tags = $issueTree->newBlob();
|
|
|
|
$tags->path("tags");
|
|
|
|
$tags->_content(join "\n", @{$self->tags});
|
|
|
|
$issueTree->add($tags);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $issueTree;
|
2018-07-04 10:46:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
1;
|