package Git::IssueManager; #ABSTRACT: Module for managing issues in a git branch within your repository use Moose; use MooseX::Privacy; 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); =attr _open B =cut has '_open' => (is => 'rw', isa => 'ArrayRef', traits => [qw/Private/]); =attr _assigned B =cut has '_assigned' => (is => 'rw', isa => 'ArrayRef', traits => [qw/Private/]); =attr _inprogress B =cut has '_inprogress' => (is => 'rw', isa => 'ArrayRef', traits => [qw/Private/]); =attr _closed B =cut has '_closed' => (is => 'rw', isa => 'ArrayRef', traits => [qw/Private/]); =attr _root B =cut has '_root' => (is => 'rw', isa => 'ArrayRef', traits => [qw/Private/]); =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 _load B called to load all the git trees =cut private_method _load => sub { my $self = shift; my @root = $self->repository->getTree("issues"); $self->_root(\@root); for my $a (@root) { $a->{path} = $a->{name}; } my @open = $self->repository->getTree("issues","open/"); $self->_open(\@open); for my $a (@open) { $a->{path} = $a->{name}; } my @closed = $self->repository->getTree("issues","closed/"); $self->_closed(\@closed); for my $a (@closed) { $a->{path} = $a->{name}; } my @assigned = $self->repository->getTree("issues","assigned/"); $self->_assigned(\@assigned); for my $a (@assigned) { $a->{path} = $a->{name}; } my @inprogress = $self->repository->getTree("issues","inprogess/"); $self->_inprogress(\@inprogress); for my $a (@inprogress) { $a->{path} = $a->{name}; } }; =method _createTree B creates the root tree for the issues branch and returns its hash =cut private_method _createTree => sub { my $self = shift; # now recreate tree structure my $openhash = $self->repository->createTree($self->_open); my $closedhash = $self->repository->createTree($self->_closed); my $assignedhash = $self->repository->createTree($self->_assigned); my $inprogresshash= $self->repository->createTree($self->_inprogress); my $openfound=0; my $closedfound=0; my $assignedfound=0; my $inprogressfound=0; # now recreate the root tree my @fordelete; my $i=0; for my $r (@{$self->_root}) { if ($r->{name} eq "open") { if (defined($openhash) && length($openhash)==40) { $r->{ref}=$openhash; $openfound=1; } else { push(@fordelete, $i); } } if ($r->{name} eq "closed") { if (defined($closedhash) && length($closedhash)==40) { $r->{ref}=$closedhash; $closedfound=1; } else { push(@fordelete, $i); } } if ($r->{name} eq "assigned") { if (defined($assignedhash) && length($assignedhash)==40) { $r->{ref}=$assignedhash; $assignedfound=1; } else { push(@fordelete, $i); } } if ($r->{name} eq "inprogess") { if (defined($inprogresshash) && length($inprogresshash)==40) { $r->{ref}=$inprogresshash; $inprogressfound=1; } else { push(@fordelete, $i); } } $i++; } for my $d (@fordelete) { splice @{$self->_root}, $d, 1; } if (!$openfound && defined($openhash)) { my $t = { path => "open", ref => $openhash, type => "tree", mode => "040000" }; push(@{$self->_root}, $t); } if (!$closedfound && defined($closedhash)) { my $t = { path => "closed", ref => $closedhash, type => "tree", mode => "040000" }; push(@{$self->_root}, $t); } if (!$assignedfound && defined($assignedhash)) { my $t = { path => "assigned", ref => $assignedhash, type => "tree", mode => "040000" }; push(@{$self->_root}, $t); } if (!$inprogressfound && defined($inprogresshash)) { my $t = { path => "inprogress", ref => $inprogresshash, type => "tree", mode => "040000" }; push(@{$self->_root}, $t); } return $self->repository->createTree($self->_root); }; =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"; $self->_load(); my $issues; if ($issue->status eq "open") { $issues=$self->_open(); } elsif ($issue->status eq "closed") { $issues=$self->_closed(); } elsif ($issue->status eq "assigned") { $issues=$self->_assigned; } elsif ($issue->status eq "inprogress") { $issues=$self->_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" }); my $roothash = $self->_createTree(); # 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)); $issue->estimated_time($self->repository->getBlob($self->repository->getFileRef("issues",$i->{path}."/estimated"))); $issue->working_time($self->repository->getBlob($self->repository->getFileRef("issues",$i->{path}."/working"))); 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; } =method delete delete the issue with the given id =cut sub delete { my $self = shift; my $id = shift; die("no id given") unless defined($id); $id=~/^[A-Z]+-(.*)$/; my $hash=$1; $self->_load(); my $found=0; my $index=0; # first search for the id for my $i (@{$self->_open}) { if (substr($i->{ref},0,8) eq $hash) { $found=1; last; } $index++; } if ($found) { splice @{$self->_open},$index,1; } $index=0; if (!$found) { for my $i (@{$self->_assigned}) { if (substr($i->{ref},0,8) eq $hash) { $found=1; last; } $index++; } if ($found) { splice @{$self->_assigned},$index,1; } } $index=0; if (!$found) { for my $i (@{$self->_inprogress}) { if (substr($i->{ref},0,8) eq $hash) { $found=1; last; } $index++; } if ($found) { splice @{$self->_inprogress},$index,1; } } $index=0; if (!$found) { for my $i (@{$self->_closed}) { if (substr($i->{ref},0,8) eq $hash) { $found=1; last; } $index++; } if ($found) { splice @{$self->_closed},$index,1; } } my $roothash = $self->_createTree(); # commit the issue my $commit=$self->repository->createTreeCommit($roothash,$self->repository->getBranchRef("heads/issues") || "start", "RM: " . $id); #now update branch refs/heads/issues $self->repository->updateRef("refs/heads/issues",$commit); } =method close set the given issue as closed =cut sub close { my $self = shift; my $id = shift; die("no id given") unless defined($id); $id=~/^[A-Z]+-(.*)$/; my $hash=$1; $self->_load(); my $found=0; my $index=0; # check if issue is already closed for my $i (@{$self->_closed}) { if (substr($i->{ref},0,8) eq $hash) { $found=1; last; } $index++; } if ($found) { die("issue is already closed"); } $index=0; # search for the id in other status arrays for my $i (@{$self->_open}) { if (substr($i->{ref},0,8) eq $hash) { $found=1; last; } $index++; } if ($found) { push(@{$self->_closed}, $self->_open()->[$index]); splice @{$self->_open},$index,1; } $index=0; for my $i (@{$self->_assigned}) { if (substr($i->{ref},0,8) eq $hash) { $found=1; last; } $index++; } if ($found) { push(@{$self->_closed}, $self->_assigned()->[$index]); splice @{$self->_assigned},$index,1; } $index=0; for my $i (@{$self->_inprogress}) { if (substr($i->{ref},0,8) eq $hash) { $found=1; last; } $index++; } if ($found) { push(@{$self->_closed}, $self->_inprogress()->[$index]); splice @{$self->_inprogress},$index,1; } my $roothash = $self->_createTree(); # commit the issue my $commit=$self->repository->createTreeCommit($roothash,$self->repository->getBranchRef("heads/issues") || "start", "Close: " . $id); #now update branch refs/heads/issues $self->repository->updateRef("refs/heads/issues",$commit); } 1;