package Git::IssueManager; #ABSTRACT: Module for managing issues in a git branch within your repository use Moose; use MooseX::Privacy; use DateTime; use DateTime::TimeZone; use Data::Dumper; use Git::LowLevel; use Git::IssueManager::Issue; =head1 DESCRIPTION Git::IssueManager is a Perl Module for using git as an issue store creating a B. It uses the Git::LowLevel Module to store issues in a B branch using trees and blobs. =head2 EXAMPLE use Git::IssueManager; my $manager = Git::IssueManager->new(repository=>Git::LowLevel->new(git_dir=> ".")); if (!$manager->ready) { print("IssueManager not initialized yet. Please call \"init\" command to do so."); exit(-1); } my @issues=$manager->list(); for my $i (@issues) { print $i->subject . "\n"; } =head2 MOTIVATION Issue management is an essential part in modern software engineering. In most cases tools like I or I are used for this task. The central nature of these tools is a large disadvantage if you are often on the road. Furthermore if you are using I for version control you have everything available for B. B =over 12 =item save your issues within your project =item manage issues on the road, without internet access =item write your own scripts for issue management =back B =over 12 =item no easy way to let users add issues without pull request yet =item not all functions implemented yet =back =head2 FEATURES =over 12 =item add issues =item list issues =item assign workers to an issue =item start and close issues =item delete issues =back =cut =attr gitcmd the path to the git command, default is using your path =cut has 'gitcmd' => (is => 'ro', isa => 'Str', default=>"git"); =attr repository Git::Repository object on which to do the issue management =cut has 'repository' => (is =>'rw', isa => 'Git::LowLevel'); =attr _open B =cut has '_open' => (is => 'rw', isa => 'Git::LowLevel::Tree', traits => [qw/Private/]); =attr _assigned B =cut has '_assigned' => (is => 'rw', isa => 'Git::LowLevel::Tree', traits => [qw/Private/]); =attr _inprogress B =cut has '_inprogress' => (is => 'rw', isa => 'Git::LowLevel::Tree', traits => [qw/Private/]); =attr _closed B =cut has '_closed' => (is => 'rw', isa => 'Git::LowLevel::Tree', traits => [qw/Private/]); =attr _root B =cut has '_root' => (is => 'rw', isa => 'Git::LowLevel::Tree', traits => [qw/Private/]); =method ready validates if everything is in place for issue management =cut sub ready { my $self = shift; my $ref = $self->repository->getReference('refs/heads/issues'); return 0 unless $ref->exist; my $version = $ref->find('.version'); return 0 unless defined($version) && ref($version) eq "Git::LowLevel::Blob"; my $tag = $ref->find('.tag'); return 0 unless defined($tag) && ref($tag) eq "Git::LowLevel::Blob"; 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 $ref = $self->repository->getReference('refs/heads/issues'); my $version = $ref->find(".version"); return $version->content; } =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 $ref = $self->repository->getReference('refs/heads/issues'); my $tag = $ref->find(".tag"); return $tag->content; } =method available_types returns an array of available/supported issue types =cut sub available_types { my $self = shift; return ("bug","security-bug","improvement","feature","task","epic"); } =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 defined($issue_tag) && length($issue_tag) > 0; my $ref = $self->repository->getReference('refs/heads/issues'); my $root = $ref->getTree(); #check if version and tag file already exist my $checkVersion=$root->find(".version"); my $checkTag=$root->find(".tag"); die("repository already has an initialized issue manager") unless !(defined($checkVersion) && defined($checkTag)); my $version = $root->newBlob(); $version->path(".version"); $version->_content("0.1"); $root->add($version); my $tag = $root->newBlob(); $tag->path(".tag"); $tag->_content($issue_tag); $root->add($tag); $ref->commit("initialized issue manager"); } =method add add an issue to the repository first paramter is an GitIssueManager::Issue object =cut sub add { my $self = shift; my $issue = shift; die("IssueManager not initialized") unless $self->ready(); die("no issue given") unless defined($issue) && ref($issue) eq "Git::IssueManager::Issue"; my $ref = $self->repository->getReference('refs/heads/issues'); my $root = $ref->getTree(); #check if issue with this subject already exist my $checkIssue=$root->find($issue->subject); die("issue with subject \"" .$issue->subject . "\" already exist") unless !(defined($checkIssue)); my $issueTree=$issue->createIssue($self->repository); my $base=$root->find($issue->status); if (!defined($base)) { $base = $root->newTree(); $base->path($issue->status); $root->add($base); } $base->add($issueTree); $ref->commit("added issue " . $issue->subject); } =method parseIssue parsed the given Git::LowLevel::Tree object as an Issue =cut sub parseIssue { my $self = shift; my $d = shift; my $tag = shift; my $status = shift; my $subject = $d->find("subject"); my $description = $d->find("description"); my $priority = $d->find("priority"); my $severity = $d->find("severity"); my $type = $d->find("type"); my $worker = $d->find("worker"); my $substatus = $d->find("substatus"); my $comment = $d->find("comment"); my $estimated = $d->find("estimated"); my $working = $d->find("working"); my $tags = $d->find("tags"); my $close_commit = $d->find("close_commit"); my $id = $tag . "-" . substr($subject->hash(),0,8); my $cd = $d->find("creation_date"); my $ld = $d->find("last_change_date"); my $cld = $d->find("closed_date"); my $author = $d->committer(); # check for required attributes die("description not available for issue " . $id) unless defined($description); die("priority not available for issue " . $id) unless defined($priority); die("severity not available for issue " . $id) unless defined($severity); die("type not available for issue " . $id) unless defined($type); #required for backwards compatibility if (!defined($cd)) { $cd = $d->timestamp_added(); } else { $cd = $cd->content(); } #required for backwards compatibility if (!defined($ld)) { $ld = $d->timestamp_last(); } else { $ld = $ld->content(); } my $tz=DateTime::TimeZone->new( name => 'local' ); my $issue = Git::IssueManager::Issue->new(subject => $subject->content); $issue->status($status); $issue->description($description->content()); $issue->priority($priority->content()); $issue->severity($severity->content()); $issue->type($type->content()); $issue->id($id); $issue->creation_date(DateTime->from_epoch( epoch =>$cd, time_zone=>$tz)); $issue->last_change_date(DateTime->from_epoch( epoch =>$ld, time_zone=>$tz)); if (defined($cld)) { $issue->closed_date(DateTime->from_epoch( epoch =>$cld->content, time_zone=>$tz)); } if (defined($close_commit) && $issue->status eq "closed") { $issue->close_commit($close_commit->content()); } if (defined($worker)) { $worker->content()=~/^(.*)\<(.*)\>$/; $issue->worker($1); $issue->worker_email($2); } if (defined($author)) { $author=~/^(.*)\<(.*)\>$/; $issue->author($1); $issue->author_email($2); } if (defined($tags)) { my @tag_array=split /\n/,$tags; $issue->tags(\@tag_array); } return $issue; } sub list { my $self = shift; my @issues; die("IssueManager not initialized") unless $self->ready(); my $ref = $self->repository->getReference('refs/heads/issues'); my $root = $ref->getTree(); my $open = $ref->find("open"); my $closed = $ref->find("closed"); my $assigned = $ref->find("assigned"); my $inprogress = $ref->find("inprogess"); my $tag = $ref->find(".tag")->content(); my @all; my @statusse = ("open","closed","assigned","inprogress"); for my $s (@statusse) { for my $status ($root->find($s)) { for my $i ($status->get()) { my $issue = $self->parseIssue($i,$tag,$s); push(@issues,$issue); } } } return @issues; } =method delete delete an issue from the issue list =cut sub delete { my $self = shift; my $id = shift; my @statusse = ("open","closed","assigned","inprogress"); die("IssueManager not initialized") unless $self->ready(); my $ref = $self->repository->getReference('refs/heads/issues'); my $tag = $ref->find(".tag")->content(); my $root = $ref->getTree(); for my $s (@statusse) { for my $status ($root->find($s)) { for my $i ($status->get()) { if (ref($i) eq "Git::LowLevel::Tree") { my $subject = $i->find("subject"); my $mytag = $tag . "-" . substr($subject->hash(),0,8); if ($id eq $mytag) { $status->del($i); $ref->commit("removed issue " . $i->mypath); return; } } } } } die("issue " . $id . " not found\n"); } =method changeStatus set status of an issue =cut sub changeStatus { my $self = shift; my $id = shift; my $status= shift; die("unknown status") unless $status eq "open" || $status eq "closed" || $status eq "inprogress" || $status eq "assigned"; my @statusse = ("open","assigned","inprogress","closed"); die("IssueManager not initialized") unless $self->ready(); my $ref = $self->repository->getReference('refs/heads/issues'); my $tag = $ref->find(".tag")->content(); my $root = $ref->getTree(); for my $s (@statusse) { next unless $s ne $status; for my $stat ($root->find($s)) { for my $i ($stat->get()) { if (ref($i) eq "Git::LowLevel::Tree") { my $subject = $i->find("subject"); my $mytag = $tag . "-" . substr($subject->hash(),0,8); if ($id eq $mytag) { my $base=$root->find($status); $stat->del($i); my $issue = $self->parseIssue($i, $tag, $s); $issue->status($status); my $issueTree=$issue->createIssue($self->repository); if (!defined($base)) { $base = $root->newTree(); $base->path($status); $root->add($base); } $base->add($issueTree); $ref->commit("closed issue " . $i->mypath); return; } } } } } die("issue " . $id . " not found\n"); } =method assign assign a worker to an issue =cut sub assign { my $self = shift; my $id = shift; my $worker_name = shift; my $worker_email = shift; my $status = "assigned"; my @statusse = ("open","assigned","inprogress","closed"); die("IssueManager not initialized") unless $self->ready(); my $ref = $self->repository->getReference('refs/heads/issues'); my $tag = $ref->find(".tag")->content(); my $root = $ref->getTree(); for my $s (@statusse) { next unless $s ne $status; for my $stat ($root->find($s)) { for my $i ($stat->get()) { if (ref($i) eq "Git::LowLevel::Tree") { my $subject = $i->find("subject"); my $mytag = $tag . "-" . substr($subject->hash(),0,8); if ($id eq $mytag) { my $base=$root->find($status); $stat->del($i); my $issue = $self->parseIssue($i, $tag, $s); $issue->status($status); $issue->worker($worker_name); $issue->worker_email($worker_email); my $issueTree=$issue->createIssue($self->repository); if (!defined($base)) { $base = $root->newTree(); $base->path($status); $root->add($base); } $base->add($issueTree); $ref->commit("closed issue " . $i->mypath); return; } } } } } die("issue " . $id . " not found\n"); } =method close close an issue =cut sub close { my $self = shift; my $id = shift; my $commit = shift || ""; my $status = "closed"; my @statusse = ("open","assigned","inprogress","closed"); die("IssueManager not initialized") unless $self->ready(); my $ref = $self->repository->getReference('refs/heads/issues'); my $tag = $ref->find(".tag")->content(); my $root = $ref->getTree(); for my $s (@statusse) { next unless $s ne $status; for my $stat ($root->find($s)) { for my $i ($stat->get()) { if (ref($i) eq "Git::LowLevel::Tree") { my $subject = $i->find("subject"); my $mytag = $tag . "-" . substr($subject->hash(),0,8); if ($id eq $mytag) { my $base=$root->find($status); $stat->del($i); my $issue = $self->parseIssue($i, $tag, $s); $issue->status($status); $issue->close_commit($commit); my $issueTree=$issue->createIssue($self->repository); if (!defined($base)) { $base = $root->newTree(); $base->path($status); $root->add($base); } $base->add($issueTree); $ref->commit("closed issue " . $i->mypath); return; } } } } } die("issue " . $id . " not found\n"); } =method open open an issue =cut sub open { my $self = shift; my $id = shift; $self->changeStatus($id, "open"); } =method start start an issue =cut sub start { my $self = shift; my $id = shift; $self->changeStatus($id, "inprogress"); } =method stats calculate some basic statistics from the current issue repository at the moment: number of open issues number of assigned issues number of inprogress issues number of closed issues @param none @return hash with {open} {assigned} {inprogress} {closed} {all} =cut sub stats { my $self = shift; my $ref = $self->repository->getReference('refs/heads/issues'); my $open = $ref->find("open"); my $closed = $ref->find("closed"); my $assigned = $ref->find("assigned"); my $inprogress = $ref->find("inprogess"); my $ret={}; # get number of all open issues if (defined($open)) { $ret->{open}=int($open->get); } else { $ret->{open}=0; } # get number of all assigned issues if (defined($assigned)) { $ret->{assigned}=int($assigned->get); } else { $ret->{assigned}=0; } # get number of all inprogress issues if (defined($inprogress)) { $ret->{inprogress}=int($inprogress->get); } else { $ret->{inprogess}=0; } # get number of all closed issues if (defined($closed)) { $ret->{closed}=int($closed->get); } else { $ret->{closed}=0; } return $ret; } =method get return an issue identified by its id @param 1. param = id @return Git::IssueManager::Issue object =cut sub get { my $self = shift; my $id = shift; my @statusse = ("open","assigned","inprogress","closed"); die("IssueManager not initialized") unless $self->ready(); my $ref = $self->repository->getReference('refs/heads/issues'); my $tag = $ref->find(".tag")->content(); my $root = $ref->getTree(); for my $s (@statusse) { for my $stat ($root->find($s)) { for my $i ($stat->get()) { if (ref($i) eq "Git::LowLevel::Tree") { my $subject = $i->find("subject"); my $mytag = $tag . "-" . substr($subject->hash(),0,8); if ($id eq $mytag) { my $issue = $self->parseIssue($i, $tag, $s); return $issue; } } } } } die("issue " . $id . " not found\n"); } =method modify modify an existing issues @param 1. parameter = issue id @param 2. parameter = modification hash support keys subject, description, priority, severity, type =cut sub modify { my $self = shift; my $id = shift; my $modifications = shift; my @statusse = ("open","assigned","inprogress","closed"); die("IssueManager not initialized") unless $self->ready(); my $ref = $self->repository->getReference('refs/heads/issues'); my $tag = $ref->find(".tag")->content(); my $root = $ref->getTree(); for my $s (@statusse) { for my $stat ($root->find($s)) { for my $i ($stat->get()) { if (ref($i) eq "Git::LowLevel::Tree") { my $subject = $i->find("subject"); my $mytag = $tag . "-" . substr($subject->hash(),0,8); if ($id eq $mytag) { $stat->del($i); my $issue = $self->parseIssue($i, $tag, $s); # modify the issue if (defined($modifications->{subject})) { $issue->subject($modifications->{subject}); } if (defined($modifications->{description})) { $issue->description($modifications->{description}); } if (defined($modifications->{priority})) { $issue->priority($modifications->{priority}); } if (defined($modifications->{severity})) { $issue->severity($modifications->{severity}); } if (defined($modifications->{type})) { $issue->type($modifications->{type}); } my $issueTree=$issue->createIssue($self->repository); $stat->add($issueTree); $ref->commit("modified issue " . $i->mypath); return; } } } } } die("issue " . $id . " not found\n"); } 1;