summaryrefslogtreecommitdiff
path: root/bin/cil
diff options
context:
space:
mode:
Diffstat (limited to 'bin/cil')
-rwxr-xr-xbin/cil425
1 files changed, 365 insertions, 60 deletions
diff --git a/bin/cil b/bin/cil
index d5a0af1..b8bcd20 100755
--- a/bin/cil
+++ b/bin/cil
@@ -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