From 60010249eeee32c13877aaaa3a5302202d3bd53e Mon Sep 17 00:00:00 2001 From: Dominik Meyer Date: Wed, 4 Jul 2018 10:46:13 +0200 Subject: [PATCH] ADD: added issue class --- lib/Git/Issue/Manager.pm | 6 - lib/Git/IssueManager.pm | 442 ++++++++++++++++++++++++++++ lib/Git/IssueManager/Issue.pm | 530 ++++++++++++++++++++++++++++++++++ 3 files changed, 972 insertions(+), 6 deletions(-) delete mode 100644 lib/Git/Issue/Manager.pm create mode 100644 lib/Git/IssueManager.pm create mode 100644 lib/Git/IssueManager/Issue.pm diff --git a/lib/Git/Issue/Manager.pm b/lib/Git/Issue/Manager.pm deleted file mode 100644 index 0fa7990..0000000 --- a/lib/Git/Issue/Manager.pm +++ /dev/null @@ -1,6 +0,0 @@ -package Git::Issue::Manager; -use strict; -use warnings; - - - 1; diff --git a/lib/Git/IssueManager.pm b/lib/Git/IssueManager.pm new file mode 100644 index 0000000..b7be90d --- /dev/null +++ b/lib/Git/IssueManager.pm @@ -0,0 +1,442 @@ +package Git::IssueManager; +#ABSTRACT: Module for managing issues in a git branch within your repository +use Moose; +use Git::RepositoryHL; +use DateTime; +use DateTime::TimeZone; +use Data::Dumper; + +our $VERSION = "0.1"; + +=attr repository + +Git::Repository object on which to do the issue management + +=cut +has 'repository' => (is =>'ro', isa => 'Git::RepositoryHL', required => 1); + + +=method ready + +validates if everything is in place for issue management + +=cut +sub ready +{ + my $self = shift; + + # return false if issue branch does not exist + return 0 unless $self->repository->hasBranch('heads/issues'); + + return 1; +} + + +=method version + +returns the version number of the issue system within the issue branch + +=cut +sub version +{ + my $self = shift; + + return unless $self->ready(); + + my @tree = $self->repository->getTree("issues"); + + for my $t (@tree) + { + if ($t->{name} eq ".version") + { + my $version = $self->repository->getBlob($t->{ref}); + chomp($version); + return $version; + } + + } + return; +} + +=method tag + +returns the issue tag to prepend in front of all issue ids + +=cut +sub tag +{ + my $self = shift; + + return unless $self->ready(); + + my @tree = $self->repository->getTree("issues"); + + for my $t (@tree) + { + if ($t->{name} eq ".tag") + { + my $tag = $self->repository->getBlob($t->{ref}); + chomp($tag); + return $tag; + } + + } + return; +} + + +=method init + +initialize the repository for managing issues + +=cut +sub init +{ + my $self = shift; + my $issue_tag = shift; + + return unless ! $self->ready(); + + die("no issue tag given") unless $issue_tag; + + # create a version file in the issues branch, also creating the branch with this + my $filehash = $self->repository->createFileObject("0.1"); + + # add the version file to a git tree + my $t = { + mode => "100644", + type => "blob", + ref => $filehash, + path => ".version" + }; + my @tree; + push(@tree,$t); + + # create a tag file in the issues branch to identify the issue tag to prepend + $filehash = $self->repository->createFileObject($issue_tag); + my $t2 = { + mode => "100644", + type => "blob", + ref => $filehash, + path => ".tag" + }; + push(@tree,$t2); + + # create the tree + my $tree_hash=$self->repository->createTree(\@tree); + + # commit the created tree + my $commit=$self->repository->createTreeCommit($tree_hash, "1", "initialized issue manager for this repository"); + + # now update/create the branch refs/heads/issues + + $self->repository->updateRef("refs/heads/issues",$commit); + +} + +=method _recreate - internal method, do not call directly + +method used to recreate the tree structure received by getTree to something like createTree can use + +=cut +sub recreate +{ + my $self = shift; + my $array = shift; + + for my $a (@{$array}) + { + $a->{path} = $a->{name}; + } +} + +=method add + +add an issue to the repository + +=cut +sub add +{ + my $self = shift; + my $issue = shift; + die("IssueManager not initialized") unless $self->ready(); + die("no Git::IssueManager::Issue object given") unless ref($issue) eq "Git::IssueManager::Issue"; + + my @root = $self->repository->getTree("issues"); + my @open = $self->repository->getTree("issues","open/"); + my @closed = $self->repository->getTree("issues","closed/"); + my @assigned = $self->repository->getTree("issues","assigned/"); + my @inprogress = $self->repository->getTree("issues","inprogress"); + + $self->recreate(\@open); + $self->recreate(\@closed); + $self->recreate(\@assigned); + $self->recreate(\@inprogress); + + my $issues; + if ($issue->status eq "open") + { + $issues=\@open; + } + elsif ($issue->status eq "closed") + { + $issues=\@closed; + } + elsif ($issue->status eq "assigned") + { + $issues=\@assigned; + } + elsif ($issue->status eq "inprogress") + { + $issues=\@inprogress; + } + + # check if issues already exist, only checks the subject !!! + for my $i (@{$issues}) + { + if ($i->{name} eq $issue->subject) + { + die("issue already exist"); + } + } + + # append the issue to the tree + my $hash=$issue->createIssue($self->repository); + push (@{$issues},{ + path => $issue->subject, + ref => $hash, + type => "tree", + mode => "040000" + }); + + # now recreate tree structure + my $openhash = $self->repository->createTree(\@open); + my $closedhash = $self->repository->createTree(\@closed); + my $assignedhash = $self->repository->createTree(\@assigned); + my $inprogresshash= $self->repository->createTree(\@inprogress); + + my $openfound=0; + my $closedfound=0; + my $assignedfound=0; + my $inprogressfound=0; + # now recreate the root tree + for my $r (@root) + { + if ($r->{name} eq "open") + { + $r->{ref}=$openhash; + $openfound=1; + } + + if ($r->{name} eq "closed") + { + $r->{ref}=$closedhash; + $closedfound=1; + } + + if ($r->{name} eq "assigned") + { + $r->{ref}=$assignedhash; + $assignedfound=1; + } + + if ($r->{name} eq "inprogess") + { + $r->{ref}=$inprogresshash; + $inprogressfound=1; + } + } + + + if (!$openfound && defined($openhash)) + { + my $t = { + path => "open", + ref => $openhash, + type => "tree", + mode => "040000" + }; + push(@root, $t); + } + + if (!$closedfound && defined($closedhash)) + { + my $t = { + path => "closed", + ref => $closedhash, + type => "tree", + mode => "040000" + }; + push(@root, $t); + } + + if (!$assignedfound && defined($assignedhash)) + { + my $t = { + path => "assigned", + ref => $assignedhash, + type => "tree", + mode => "040000" + }; + push(@root, $t); + } + + if (!$inprogressfound && defined($inprogresshash)) + { + my $t = { + path => "inprogress", + ref => $inprogresshash, + type => "tree", + mode => "040000" + }; + push(@root, $t); + } + + my $roothash = $self->repository->createTree(\@root); + + # commit the issue + my $commit=$self->repository->createTreeCommit($roothash,$self->repository->getBranchRef("heads/issues") || "start", "ADD: " . $issue->subject); + + #now update branch refs/heads/issues + $self->repository->updateRef("refs/heads/issues",$commit); + +} + +=method _to_issue - internal method, do not call directly + +returns the issue converted from a tree hash entry + +=cut +sub _to_issue +{ + my $self = shift; + my $i = shift; + + my $issue = Git::IssueManager::Issue->new(subject => $i->{name}); + + $issue->priority($self->repository->getBlob($self->repository->getFileRef("issues",$i->{path}."/priority"))); + $issue->severity($self->repository->getBlob($self->repository->getFileRef("issues",$i->{path}."/severity"))); + $issue->type($self->repository->getBlob($self->repository->getFileRef("issues",$i->{path}."/type"))); + $issue->description($self->repository->getBlob($self->repository->getFileRef("issues",$i->{path}."/description"))); + $issue->comment($self->repository->getBlob($self->repository->getFileRef("issues",$i->{path}."/comment"))); + $issue->id($self->repository->getBlob($self->repository->getFileRef("issues",".tag")) . "-" . substr($i->{ref},0,8)); + + my $worker = $self->repository->getBlob($self->repository->getFileRef("issues",$i->{path}."/worker")); + if (defined($worker)) + { + $worker=~/^(.*)<(.*)>$/; + $issue->worker($1 || ""); + $issue->worker_email($2 || ""); + } + + my $taglist= $self->repository->getBlob($self->repository->getFileRef("issues",$i->{path}."/tags")); + if (defined($taglist) && length($taglist)>0) + { + my @tags=split /\n/,$taglist; + $issue->tags(\@tags); + } + + my $tz=DateTime::TimeZone->new( name => 'local' ); + my @commit = $self->repository->getFileLog("issues",$i->{path}."/subject",1); + $issue->creation_date(DateTime->from_epoch(epoch => $commit[0]->{date},time_zone=>$tz )); + $issue->author($commit[0]->{author}->{name}); + $issue->author_email($commit[0]->{author}->{email}); + + @commit = $self->repository->getFileLog("issues",$i->{path},1); + $issue->last_change_date(DateTime->from_epoch(epoch => $commit[0]->{date},time_zone=>$tz )); + + $issue->closed_date(DateTime->from_epoch(epoch => $commit[0]->{date},time_zone=>$tz )); + + return $issue; +} + + +=method get + +return the issue with the given id + +=cut +sub get +{ + my $self = shift; + my $id = shift; + + die("no id given") unless defined($id); + + $id=~/^[A-Z]+-(.*)$/; + my $hash=$1; + + # first search for the id + my @open = $self->repository->getTree("issues","open/"); + for my $i (@open) + { + if (substr($i->{ref},0,8) eq $hash) + { + return $self->_to_issue($i); + } + } + + my @assigned = $self->repository->getTree("issues","assigned/"); + for my $i (@assigned) + { + if (substr($i->{ref},0,8) eq $hash) + { + return $self->_to_issue($i); + } + } + + my @inprogress = $self->repository->getTree("issues","inprogress"); + for my $i (@inprogress) + { + if (substr($i->{ref},0,8) eq $hash) + { + return $self->_to_issue($i); + } + } + + my @closed = $self->repository->getTree("issues","closed/"); + for my $i (@closed) + { + if (substr($i->{ref},0,8) eq $hash) + { + return $self->_to_issue($i); + } + } + + return; +} + + + +=method list + +list all issues of the repository + +=cut +sub list +{ + my $self = shift; + my $filter = shift; + my @issues =$self->repository->getTree("issues","open/"); + my @ret; + + if (!defined($filter) || $filter eq "open" ) + { + @issues=$self->repository->getTree("issues","open/"); + } + + for my $i (@issues) + { + my $issue = $self->_to_issue($i); + + push(@ret, $issue); + } + + # now sort by creation date + @ret = sort {DateTime->compare($a->creation_date, $b->creation_date)} @ret; + + return @ret; +} + + +1; diff --git a/lib/Git/IssueManager/Issue.pm b/lib/Git/IssueManager/Issue.pm new file mode 100644 index 0000000..d0fa1a7 --- /dev/null +++ b/lib/Git/IssueManager/Issue.pm @@ -0,0 +1,530 @@ +package Git::IssueManager::Issue; +#ABSTRACT: class representing an Issue +use Moose; +use File::Basename; + +=head1 DESCRIPTION + +B 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 + +=item I - the most highes level of priority + +=item I + +=item I + +=item I + +=back + +The default value is B. + +=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 + +=item I + +=item I + +=item I + +=back + +The default value is B + +=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 - a problem within the code, preventing the correct working of the software + +=item I - a security related problem within the code, preventing the correct working of the software + +=item I - an enhancement to an already existing feature + +=item I - a completly new feature + +=item I - a simple task, which should be done (please use rarely) + +=back + +The default values is B. + +=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" || + lc($new) eq "task"; +}); + +=attr status + +The status of the issue. Possible values are: + +=over + +=item I - nothing has been done yet + +=item I - the issue has been assigned to a developer + +=item I - somebody is working on the issue + +=item I - the issue is closed + +=back + +The default value is B. + +=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 - there is no substatus + +=item I - the bug was fixed + +=item I - the issue has been closed but it will never be fixed + +=back + +The default value is B. + +=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 + +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 => ""); + + +=method addTag + +add another tag to the issue. + +B + + $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 + + $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 + + $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 + + $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; + my @tree; + + die("No Git::RepositoryHL object given") unless ref($repository) eq "Git::RepositoryHL"; + + my $subject = { + path => "subject", + ref => $repository->createFileObject($self->subject), + type => "blob", + mode => "100644" + }; + push(@tree, $subject); + + my $priority = { + path => "priority", + ref => $repository->createFileObject($self->priority), + type => "blob", + mode => "100644" + }; + push(@tree, $priority); + + my $severity = { + path => "severity", + ref => $repository->createFileObject($self->severity), + type => "blob", + mode => "100644" + }; + push(@tree, $severity); + + my $type = { + path => "type", + ref => $repository->createFileObject($self->type), + type => "blob", + mode => "100644" + }; + push(@tree, $type); + + my $substatus = { + path => "substatus", + ref => $repository->createFileObject($self->substatus), + type => "blob", + mode => "100644" + }; + + my $comment = { + path => "comment", + ref => $repository->createFileObject($self->comment), + type => "blob", + mode => "100644" + }; + push(@tree, $comment); + + my $description = { + path => "description", + ref => $repository->createFileObject($self->description), + type => "blob", + mode => "100644" + }; + push(@tree, $description); + + my $worker = { + path => "worker", + ref => $repository->createFileObject($self->worker . "<" . $self->worker_email . ">"), + type => "blob", + mode => "100644" + }; + push(@tree, $worker); + + + + my $tags = { + path => "tags", + ref => $repository->createFileObject(join "\n", @{$self->tags}), + type => "blob", + mode => "100644" + }; + push(@tree, $tags); + + if (@{$self->attachements} > 0) + { + my $attachments={ + path => "attachments", + ref => $self->_createAttachmentTree($repository), + type => "tree", + mode => "040000" + }; + push(@tree, $attachments); + } + + return $repository->createTree(\@tree); +} + +1;