diff options
author | Francois Marier <francois@debian.org> | 2008-06-29 21:51:31 +1200 |
---|---|---|
committer | Francois Marier <francois@debian.org> | 2008-06-29 21:51:31 +1200 |
commit | 03a60521d41962fb3d36e8e8002e9bba51796ff6 (patch) | |
tree | 36150e8a410695002674e5ad202c425d1796b0c9 /bin | |
parent | 42280f662d3ce4affb00eb68a22a081dfb951395 (diff) |
Imported Upstream version v0.3.0upstream/v0.3.0
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/cil | 425 |
1 files changed, 365 insertions, 60 deletions
@@ -20,8 +20,6 @@ use strict; use warnings; -use Data::Dumper; - use Getopt::Mixed "nextOption"; use Digest::MD5 qw(md5_hex); use File::Touch; @@ -36,42 +34,42 @@ use CIL::Attachment; ## ---------------------------------------------------------------------------- # constants -use constant VERSION => '0.2.1'; +my $y = 'y'; + +use constant VERSION => '0.3.0'; my @IN_OPTS = ( + # strings 'p=s', # p = path 'path>p', # for 'add' 'f=s', # f = filename - 'filename=f', # for 'extract' - + 'filename>f', # for 'extract' + 's=s', # s = status + 'status>s', # for 'summary', 'list' + 'l=s', # l = label + 'label>l', # for 'summary, 'list' + 'c=s', # c = created-by + 'created-by>c', # for 'summary', 'list' + 'a=s', # a = assigned_to + 'assigned-to>a',# for 'summary', 'list' + + # booleans + 'is-open', # for 'summary', 'list' + 'is-closed', # for 'summary', 'list' 'help', 'version', ); my %BOOLEAN_ARGS = ( - help => 1, - version => 1, + 'help' => 1, + 'version' => 1, + 'is-open' => 1, + 'is-closed' => 1, ); my $gan = $ENV{GIT_AUTHOR_NAME} || 'Your Name'; my $gae = $ENV{GIT_AUTHOR_EMAIL} || 'you@example.org'; -my $new_issue_text = <<"EOF"; -Summary : -Status : New -CreatedBy : $gan <$gae> -AssignedTo : $gan <$gae> -Label : - -Description... -EOF - -my $add_comment_text = <<"EOF"; -CreatedBy : $gan <$gae> - -Description... -EOF - ## ---------------------------------------------------------------------------- # main program @@ -100,6 +98,7 @@ EOF } my $cil = CIL->new(); + $cil->read_config_file( '.cil' ); &{"cmd_$command"}($cil, $args, @ARGV); } @@ -142,7 +141,7 @@ sub cmd_init { # add a README.txt so people know what this is about unless ( -f "$issues_dir/README.txt" ) { - write_file("issues_dir/README.txt", <<README); + write_file("$issues_dir/README.txt", <<'README'); This directory is used by CIL to track issues and feature requests. The home page for CIL is at http://kapiti.geek.nz/software/cil.html @@ -154,12 +153,13 @@ README } sub cmd_list { - my ($cil) = @_; + my ($cil, $args) = @_; check_paths($cil); # find all the issues my $issues = $cil->get_issues(); + $issues = filter_issues( $cil, $issues, $args ); if ( @$issues ) { foreach my $issue ( sort { $a->Inserted cmp $b->Inserted } @$issues ) { separator(); @@ -173,12 +173,13 @@ sub cmd_list { } sub cmd_summary { - my ($cil) = @_; + my ($cil, $args) = @_; check_paths($cil); # find all the issues my $issues = $cil->get_issues(); + $issues = filter_issues( $cil, $issues, $args ); if ( @$issues ) { separator(); foreach my $issue ( @$issues ) { @@ -225,11 +226,37 @@ sub cmd_status { sub cmd_add { my ($cil, undef, $issue_name) = @_; - # read in the new issue text CIL::Utils->ensure_interactive(); - my $fh = CIL::Utils->solicit( $new_issue_text ); - my $issue = CIL::Issue->new_from_fh( 'tmp', $fh ); + my $issue = CIL::Issue->new('tmpname'); + $issue->Status('New'); + $issue->CreatedBy("$gan <$gae>"); + $issue->AssignedTo("$gan <$gae>"); + $issue->Description("Description ..."); + + my $edit = $y; + + # keep going until we get a valid issue or we want to quit + while ( $edit eq $y ) { + # read in the new issue text + my $fh = CIL::Utils->solicit( $issue->as_output ); + $issue = CIL::Issue->new_from_fh( 'tmp', $fh ); + + # check if the issue is valid + if ( $issue->is_valid($cil) ) { + $edit = 'n'; + } + else { + msg($_) foreach @{ $issue->errors }; + print 'Would you like to re-edit (y/n): '; + $edit = <STDIN>; + chomp $edit; + print "\n"; + } + } + + # if the issue is still invalid, they quit without correcting it + return unless $issue->is_valid( $cil ); # we've got the issue, so let's name it my $unique_str = $issue->Inserted . $issue->Summary . $issue->Description; @@ -246,20 +273,35 @@ sub cmd_edit { fatal("Couldn't load issue '$issue_name'"); } - # create the ini file, then edit it - my $edit_issue_text = $issue->as_output; - - # read in the new issue text CIL::Utils->ensure_interactive(); - my $fh = CIL::Utils->solicit( join('', @$edit_issue_text) ); - my $issue_edited = CIL::Issue->new_from_fh( $issue->name, $fh ); - unless ( defined $issue_edited ) { - fatal("couldn't create issue (program error)"); + my $edit = $y; + + # keep going until we get a valid issue or we want to quit + while ( $edit eq $y ) { + # read in the new issue text + my $fh = CIL::Utils->solicit( $issue->as_output ); + $issue = CIL::Issue->new_from_fh( $issue->name, $fh ); + + # check if the issue is valid + if ( $issue->is_valid($cil) ) { + $edit = 'n'; + } + else { + msg($_) foreach @{ $issue->errors }; + print 'Would you like to re-edit (y/n): '; + $edit = <STDIN>; + chomp $edit; + print "\n"; + } } - $issue_edited->save($cil); - display_issue($cil, $issue_edited); + # if the issue is still invalid, they quit without correcting it + return unless $issue->is_valid( $cil ); + + # save it + $issue->save($cil); + display_issue($cil, $issue); } sub cmd_comment { @@ -270,21 +312,43 @@ sub cmd_comment { fatal("couldn't load issue '$issue_name'"); } - # read in the new issue text CIL::Utils->ensure_interactive(); - my $fh = CIL::Utils->solicit( $add_comment_text ); - my $comment = CIL::Comment->new_from_fh( 'tmp', $fh ); - unless ( defined $comment ) { - fatal("could not create new comment"); + # create the new comment + my $comment = CIL::Comment->new('tmpname'); + $comment->Issue( $issue->name ); + $comment->CreatedBy("$gan <$gae>"); + $comment->Description("Description ..."); + + my $edit = $y; + + # keep going until we get a valid issue or we want to quit + while ( $edit eq $y ) { + # read in the new comment text + my $fh = CIL::Utils->solicit( $comment->as_output ); + $comment = CIL::Comment->new_from_fh( 'tmp', $fh ); + + # check if the comment is valid + if ( $comment->is_valid($cil) ) { + $edit = 'n'; + } + else { + msg($_) foreach @{ $issue->errors }; + print 'Would you like to re-edit (y/n): '; + $edit = <STDIN>; + chomp $edit; + print "\n"; + } } + # if the comment is still invalid, they quit without correcting it + return unless $comment->is_valid( $cil ); + # we've got the comment, so let's name it my $unique_str = $comment->Inserted . $issue->Description; $comment->set_name( substr(md5_hex($unique_str), 0, 8) ); - # finally, tell it who it's parent is and then save - $comment->Issue( $issue->name ); + # finally, save it $comment->save($cil); # add the comment to the issue, update it's timestamp and save it out @@ -347,7 +411,7 @@ EOF } sub cmd_extract { - my ($cil, undef, $attachment_name, $args) = @_; + my ($cil, $args, $attachment_name) = @_; my $attachment = CIL::Attachment->new_from_name($cil, $attachment_name); unless ( defined $attachment ) { @@ -358,15 +422,184 @@ sub cmd_extract { write_file( $filename, $attachment->as_binary ); } +sub cmd_fsck { + my ($cil, $args) = @_; + + # this looks at all the issues it can find and checks for: + # * validity + # * all the comments are there + # * all the attachments are there + # then it checks each individual comment/attachment for: + # * ToDo: validity + # * it's parent exists + + check_paths($cil); + + # find all the issues, comments and attachments + my $issues = $cil->get_issues(); + my $issue = {}; + foreach my $i ( @$issues ) { + $issue->{$i->name} = $i; + } + my $comments = $cil->get_comments(); + my $comment = {}; + foreach my $c ( @$comments ) { + $comment->{$c->name} = $c; + } + my $attachments = $cil->get_attachments(); + my $attachment = {}; + foreach my $a ( @$attachments ) { + $attachment->{$a->name} = $a; + } + + my $errors = {}; + + if ( @$issues ) { + foreach my $i ( sort { $a->Inserted cmp $b->Inserted } @$issues ) { + my $name = $i->name; + + unless ( $i->is_valid($cil) ) { + foreach ( @{ $i->errors } ) { + push @{$errors->{$name}}, $_; + } + } + + # check that all it's comments are there and that they have this parent + my $comments = $i->Comments; + foreach my $c ( @$comments ) { + # see if this comment exists at all + if ( exists $comment->{$c} ) { + # check the parent is this issue + push @{$errors->{$name}}, "comment '$c' is listed under issue '" . $i->name . "' but does not reciprocate" + unless $comment->{$c}->Issue eq $i->name; + } + else { + push @{$errors->{$name}}, "comment '$c' listed in issue '" . $i->name . "' does not exist"; + } + } + + # check that all it's attachments are there and that they have this parent + my $attachments = $i->Attachments; + foreach my $a ( @$attachments ) { + # see if this attachment exists at all + if ( exists $attachment->{$a} ) { + # check the parent is this issue + push @{$errors->{$name}}, "attachment '$a' is listed under issue '" . $i->name . "' but does not reciprocate" + unless $attachment->{$a}->Issue eq $i->name; + } + else { + push @{$errors->{$name}}, "attachment '$a' listed in issue '" . $i->name . "' does not exist"; + } + } + } + } + + print_fsck_errors('Issue', $errors); + + # comments + $errors = {}; + + # loop through all the comments + if ( @$comments ) { + # check that their parent issues exist + foreach my $c ( sort { $a->Inserted cmp $b->Inserted } @$comments ) { + # check that the parent of each comment exists + unless ( exists $issue->{$c->Issue} ) { + push @{$errors->{$c->name}}, "comment '" . $c->name . "' refers to issue '" . $c->Issue . "' but issue does not exist"; + } + } + } + + print_fsck_errors('Comment', $errors); + + # attachments + $errors = {}; + + # loop through all the attachments + if ( @$attachments ) { + # check that their parent issues exist + foreach my $a ( sort { $a->Inserted cmp $b->Inserted } @$attachments ) { + # check that the parent of each attachment exists + unless ( exists $issue->{$a->Issue} ) { + push @{$errors->{$a->name}}, "attachment '" . $a->name . "' refers to issue '" . $a->Issue . "' but issue does not exist"; + } + } + } + + print_fsck_errors('Attachment', $errors); + + # nothing more to do + separator(); +} + +sub print_fsck_errors { + my ($entity, $errors) = @_; + + separator(); + foreach my $issue_name ( keys %$errors ) { + title( "$entity $issue_name "); + foreach my $error ( @{$errors->{$issue_name}} ) { + msg("* $error"); + } + } +} + ## ---------------------------------------------------------------------------- sub check_paths { my ($cil) = @_; # make sure an issue directory is available - unless ( -d $cil->issue_dir ) { - fatal("couldn't find '" . $cil->issue_dir . "' directory"); + unless ( -d $cil->IssueDir ) { + fatal("couldn't find '" . $cil->IssueDir . "' directory"); + } +} + +sub filter_issues { + my ($cil, $issues, $args) = @_; + + # don't filter if we haven't been given anything + return $issues unless %$args; + + # take a copy of the whole lot first (so we don't destroy the input list) + my @new_issues = @$issues; + + # firstly, get out the Statuses we want + if ( defined $args->{s} ) { + @new_issues = grep { $_->Status eq $args->{s} } @new_issues; + } + + # then see if we want a particular label (could be a bit nicer) + if ( defined $args->{l} ) { + my @tmp; + foreach my $issue ( @new_issues ) { + push @tmp, $issue + if grep { $_ eq $args->{l} } @{$issue->Labels}; + } + @new_issues = @tmp; + } + + # filter out dependent on open/closed + if ( defined $args->{'is-open'} ) { + # just get the open issues + @new_issues = grep { $_->is_open($cil) } @new_issues; + } + if ( defined $args->{'is-closed'} ) { + # just get the closed issues + @new_issues = grep { $_->is_closed($cil) } @new_issues; } + + # filter out 'created by' + if ( defined $args->{c} ) { + @new_issues = grep { $args->{c} eq $_->created_by_email } @new_issues; + } + + # filter out 'assigned to' + if ( defined $args->{a} ) { + @new_issues = grep { $args->{a} eq $_->assigned_to_email } @new_issues; + } + + return \@new_issues; } ## ---------------------------------------------------------------------------- @@ -535,14 +768,15 @@ Usage: $0 COMMAND [options] Commands: init [--path=PATH] add - summary - list + summary [--status=STATUS] [--label=LABEL] [--is-open] [--is-closed] + list [--status=STATUS] [--label=LABEL] [--is-open] [--is-closed] show ISSUE status ISSUE NEW_STATUS edit ISSUE comment ISSUE attach ISSUE FILENAME extract ATTACHMENT [--filename=FILENAME] + fsck See <http://kapiti.geek.nz/software/cil.html> for further information. Report bugs to <andychilton -at- gmail -dot- com>. @@ -560,6 +794,9 @@ cil - the command-line issue list $ cil init $ cil summary $ cil list + $ cil list --status=New + $ cil list --label=Release-v0.1 + $ cil list --is-open $ cil add ... added issue 'cafebabe' ... @@ -576,6 +813,8 @@ cil - the command-line issue list $ cil extract decaf7ea $ cil extract decaf7ea --filename=other_filename.txt + $ cil fsck + =head1 DESCRIPTION Cil is a small but useful command-line issue list. It saves issues, comments @@ -588,13 +827,15 @@ and attachments as local files which you can check in to your repository. Creates a local '.cil' file and an 'issues' directory. If PATH is specified, the config file and directory will be created in the destination directory. -=item summary +=item summary [--status=STATUS] [--label=LABEL] [--is-open] [--is-closed] -Displays a one line summary for each issue. +Displays a one line summary for each issue. You may filter on both the Status +and Label fields. -=item list +=item list [--status=STATUS] [--label=LABEL] [--is-open] [--is-closed] -Shows each issue with more information. +Shows each issue with more information. You may filter on both the Status and +Label fields. =item add @@ -628,19 +869,83 @@ otherwise it will use the original one saved along with the attachment. =back +=head1 .cil + +The C<.cil> file is used to configure bits and pieces within cil for this +particular issue list. The following options are available and where stated, +may be declared multiple times: + +The C<.cil> file is fairly simple and an example can be seen here: + + StatusStrict: 1 + StatusAllowedList: New + StatusAllowedList: InProgress + StatusAllowedList: Finished + StatusOpenList: New + StatusOpenList: InProgress + StatusClosedList: Finished + LabelStrict: 1 + LabelAllowedList: Type-Enhancement + LabelAllowedList: Type-Defect + LabelAllowedList: Priority-High + LabelAllowedList: Priority-Medium + LabelAllowedList: Priority-Low + +=over + +=item StatusStrict + +Default: 0, Type: Boolean (0/1) + +If this is set to a true value then cil checks that the status you enter into +an issue (after adding or editing) is also in the allowed list (see +StatusAllowedList). + +=item StatusAllowedList + +Default: empty, Type: List + +This list is checked against when adding or editing issues but only if you have +StatusStrict on. + +=item StatusOpenList + +Default: empty, Type: List + +This list is checked against when filtering with --is-open. + +=item StatusClosedList + +Default: empty, Type: List + +This list is checked against when filtering with --is-closed. + +=item LabelStrict + +Default: 0, Type: Boolean (0/1) + +This determines that labels you enter are checked against LabelAllowedList. Set +to 1 if you require this feature. + +=item LabelAllowedList + +Default: empty, Type: List + +This determines which labels are allowed if you have turned on LabelStrict. + +=back + =head1 BUGS Probably. Let me know :-) =head1 TODO -There is a number of things to do. High on the list are: - -* the ability to set Statuses from the command line +To get a ToDo list for cil, clone the repo, find the issues/ dir and type: -* set where you want your issues (from a .cil file) + $ cil --is-open -* simple search first, proper search and indexing second +This gives the current outstanding issues in cil. =head1 AUTHOR |