]> git.mjollnir.org Git - cil.git/commitdiff
Follow Yanick's lead and push all the command out into pluggable modules
authorAndrew Chilton <andychilton@gmail.com>
Mon, 22 Sep 2008 12:05:58 +0000 (00:05 +1200)
committerAndrew Chilton <andychilton@gmail.com>
Mon, 22 Sep 2008 12:05:58 +0000 (00:05 +1200)
19 files changed:
bin/cil
lib/CIL.pm
lib/CIL/Command/Add.pm [new file with mode: 0644]
lib/CIL/Command/Am.pm [new file with mode: 0644]
lib/CIL/Command/Attach.pm [new file with mode: 0644]
lib/CIL/Command/Comment.pm [new file with mode: 0644]
lib/CIL/Command/DependsOn.pm [new file with mode: 0644]
lib/CIL/Command/Edit.pm [new file with mode: 0644]
lib/CIL/Command/Extract.pm [new file with mode: 0644]
lib/CIL/Command/Fsck.pm [new file with mode: 0644]
lib/CIL/Command/Init.pm [new file with mode: 0644]
lib/CIL/Command/List.pm
lib/CIL/Command/Precedes.pm [new file with mode: 0644]
lib/CIL/Command/Show.pm [new file with mode: 0644]
lib/CIL/Command/Status.pm [new file with mode: 0644]
lib/CIL/Command/Steal.pm [new file with mode: 0644]
lib/CIL/Command/Summary.pm [new file with mode: 0644]
lib/CIL/Command/Track.pm [new file with mode: 0644]
lib/CIL/Utils.pm

diff --git a/bin/cil b/bin/cil
index 54f136a520916eff7a976e91f6f6f8987b3add66..dff5789fb0cb8356064da52d70a9690d9f3926f4 100755 (executable)
--- a/bin/cil
+++ b/bin/cil
@@ -21,7 +21,6 @@ use strict;
 use warnings;
 
 use Getopt::Mixed "nextOption";
-use Digest::MD5 qw(md5_hex);
 use File::Touch;
 use File::Glob ':glob';
 use File::Basename;
@@ -37,8 +36,6 @@ use CIL::Attachment;
 ## ----------------------------------------------------------------------------
 # constants
 
-my $y = 'y';
-
 my @IN_OPTS = (
     # strings
     'p=s',          # p = path
@@ -112,798 +109,11 @@ my %BOOLEAN_ARGS = (
     $command->run($cil, $args, @ARGV);
 }
 
-## ----------------------------------------------------------------------------
-# commands
-
-sub cmd_init {
-    my ($cil, $args) = @_;
-
-    my $path = $args->{p} || '.'; # default path is right here
-
-    # error if $path doesn't exist
-    unless ( -d $path ) {
-        fatal("path '$path' doesn't exist");
-    }
-
-    # error if issues/ already exists
-    my $issues_dir = "$path/issues";
-    if ( -d $issues_dir ) {
-        fatal("issues directory '$issues_dir' already exists, not initialising issues");
-    }
-
-    # error if .cil already exists
-    my $config = "$path/.cil";
-    if ( -f $config ) {
-        fatal("config file '$config' already exists, not initialising issues");
-    }
-
-    # try to create the issues/ dir
-    unless ( mkdir $issues_dir ) {
-        fatal("Couldn't create '$issues_dir' directory: $!");
-    }
-
-    # are we in a Git repository?
-    my $VCSconfig = '';
-    if ( -d '.git' ) {
-        msg( 'git repository detected, setting VCS accordingly' );
-        $VCSconfig = 'VCS: git';
-        my $vcs = CIL::VCS::Factory->new( 'Git' );
-    }
-
-    # create a .cil file here also
-    if ( $args->{bare} ) {
-        unless ( touch $config ) {
-            rmdir $issues_dir;
-            fatal("couldn't create a '$config' file");
-        }
-    }
-    else {
-        # write a default .cil file
-        write_file($config, <<"CONFIG");
-$VCSconfig
-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
-CONFIG
-    }
-
-    # add a README.txt so people know what this is about
-    unless ( -f "$issues_dir/README.txt" ) {
-        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
-README
-    }
-
-    # $path/issues/ and $path/.cil create correctly
-    msg("initialised empty issue list inside '$path/'");
-}
-
-sub cmd_summary {
-    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 ) {
-            display_issue_summary($cil, $issue);
-        }
-        separator();
-    }
-    else {
-        msg('no issues found');
-    }
-}
-
-sub cmd_show {
-    my ($cil, undef, $issue_name) = @_;
-
-    # firstly, read the issue in
-    my $issue = load_issue_fuzzy( $cil, $issue_name );
-    display_issue_full($cil, $issue);
-}
-
-sub cmd_status {
-    my ($cil, undef, $issue_name, $status) = @_;
-
-    unless ( defined $status ) {
-        fatal("provide a status to set this issue to");
-    }
-
-    # firstly, read the issue in
-    my $issue = load_issue_fuzzy( $cil, $issue_name );
-
-    # set the status for this issue
-    $issue->Status( $status );
-    $issue->save($cil);
-
-    display_issue($cil, $issue);
-}
-
-sub cmd_steal {
-    my ($cil, undef, $issue_name) = @_;
-
-    # firstly, read the issue in
-    my $issue = load_issue_fuzzy( $cil, $issue_name );
-
-    # set the AssignedTo for this issue to you (because you're stealing it)
-    $issue->AssignedTo( user($cil) );
-    $issue->save($cil);
-
-    display_issue($cil, $issue);
-}
-
-sub cmd_add {
-    my ($cil, undef, @argv ) = @_;
-
-    CIL::Utils->ensure_interactive();
-
-    my $user = user($cil);
-
-    my $issue = CIL::Issue->new('tmpname');
-    $issue->Summary( join ' ', @argv );
-    $issue->Status('New');
-    $issue->CreatedBy( $user );
-    $issue->Description("Description ...");
-
-    add_issue_loop($cil, undef, $issue);
-}
-
-sub cmd_edit {
-    my ($cil, undef, $issue_name) = @_;
-
-    my $issue = load_issue_fuzzy( $cil, $issue_name );
-
-    CIL::Utils->ensure_interactive();
-
-    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 };
-            $edit = ans('Would you like to re-edit (y/n): ');
-        }
-    }
-
-    # 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 {
-    my ($cil, undef, $issue_name) = @_;
-
-    my $issue = load_issue_fuzzy( $cil, $issue_name );
-
-    CIL::Utils->ensure_interactive();
-
-    # create the new comment
-    my $comment = CIL::Comment->new('tmpname');
-    $comment->Issue( $issue->name );
-    $comment->CreatedBy( user($cil) );
-    $comment->Description("Description ...");
-
-    add_comment_loop($cil, undef, $issue, $comment);
-}
-
-sub cmd_attach {
-    my ($cil, undef, $issue_name, $filename) = @_;
-
-    my $issue = load_issue_fuzzy( $cil, $issue_name );
-
-    # check to see if the file exists
-    unless ( -r $filename ) {
-        fatal("couldn't read file '$filename'");
-    }
-
-    my $basename = basename( $filename );
-    my $user = user($cil);
-
-    my $add_attachment_text = <<"EOF";
-Filename    : $basename
-CreatedBy   : $user
-
-File goes here ... this will be overwritten.
-EOF
-
-    # read in the new issue text
-    CIL::Utils->ensure_interactive();
-    my $fh = CIL::Utils->solicit( $add_attachment_text );
-
-    my $attachment = CIL::Attachment->new_from_fh( 'tmp', $fh );
-    unless ( defined $attachment ) {
-        fatal("could not create new attachment");
-    }
-
-    # now add the file itself
-    my $contents = read_file( $filename );
-    $attachment->set_file_contents( $contents );
-
-    # set the size
-    my ($size) = (stat($filename))[7];
-    $attachment->Size( $size );
-
-    # we've got the attachment, so let's name it
-    my $unique_str = time . $attachment->Inserted . $attachment->File;
-    $attachment->set_name( substr(md5_hex($unique_str), 0, 8) );
-
-    # finally, tell it who it's parent is and then save
-    $attachment->Issue( $issue->name );
-    $attachment->save($cil);
-
-    # add the comment to the issue, update it's timestamp and save it out
-    $issue->add_attachment( $attachment );
-    $issue->save($cil);
-    display_issue_full($cil, $issue);
-}
-
-sub cmd_extract {
-    my ($cil, $args, $attachment_name) = @_;
-
-    my $attachment = load_attachment_fuzzy($cil, $attachment_name);
-
-    my $filename = $args->{f} || $attachment->Filename();
-    write_file( $filename, $attachment->as_binary );
-}
-
-sub cmd_track {
-    my ($cil, $args, $issue_name) = @_;
-
-    fatal("the 'VCS' option in your .cil file is not set")
-        unless defined $cil->VCS;
-
-    fatal("the 'VCS' option currently only supports values of 'Git'")
-        unless $cil->VCS eq 'Git';
-
-    my $issue = load_issue_fuzzy($cil, $issue_name);
-
-    # add the issue to Git
-    my $issue_dir = $cil->IssueDir();
-    my @files;
-    push @files, "$issue_dir/i_" . $issue->name . '.cil';
-    push @files, map { "$issue_dir/c_${_}.cil" } @{ $issue->CommentList };
-    push @files, map { "$issue_dir/a_${_}.cil" } @{ $issue->AttachmentList };
-    msg("git add @files");
-}
-
-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;
-    }
-
-    # ------
-    # issues
-    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->CommentList;
-            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->AttachmentList;
-            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";
-                }
-            }
-
-            # check that all it's 'DependsOn' are there and that they have this under 'Precedes'
-            my $depends_on = $i->DependsOnList;
-            foreach my $d ( @$depends_on ) {
-                # see if this issue exists at all
-                if ( exists $issue->{$d} ) {
-                    # check the 'Precedes' is this issue
-                    my %precedes = map { $_ => 1 } @{$issue->{$d}->PrecedesList};
-                    push @{$errors->{$name}}, "issue '$d' should precede '" . $i->name . "' but doesn't"
-                        unless exists $precedes{$i->name};
-                }
-                else {
-                    push @{$errors->{$name}}, "issue '$d' listed as a dependency of issue '" . $i->name . "' does not exist";
-                }
-            }
-
-            # check that all it's 'Precedes' are there and that they have this under 'DependsOn'
-            my $precedes = $i->PrecedesList;
-            foreach my $p ( @$precedes ) {
-                # see if this issue exists at all
-                if ( exists $issue->{$p} ) {
-                    # check the 'DependsOn' is this issue
-                    my %depends_on = map { $_ => 1 } @{$issue->{$p}->DependsOnList};
-                    push @{$errors->{$name}}, "issue '$p' should depend on '" . $i->name . "' but doesn't"
-                        unless exists $depends_on{$i->name};
-                }
-                else {
-                    push @{$errors->{$name}}, "issue '$p' listed as preceding 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 left
-    separator();
-}
-
-sub cmd_am {
-    my ($cil, $args, $email_filename) = @_;
-
-    unless ( -f $email_filename ) {
-        fatal("couldn't load email '$email_filename'");
-    }
-
-    my $msg_text = read_file($email_filename);
-
-    my $email = Email::Simple->new($msg_text);
-    unless ( defined $email ) {
-        fatal("email file '$email_filename' didn't look like an email");
-    }
-
-    # extract some fields
-    my $subject = $email->header('Subject');
-    my $from    = $email->header('From');
-    my $date    = find_date($email)->datetime;
-    my $body    = $email->body;
-
-    # see if we can find any issue names in either the subject or the body
-    my @issue_names;
-    foreach my $text ( $subject, $body ) {
-        my @new = ( $text =~ /\b\#?([0-9a-f]{8})\b/gxms );
-        push @issue_names, @new;
-    }
-
-    msg("Found possible issue names in email: ", ( join(' ', @issue_names) || '[none]' ));
-
-    my %issue;
-    foreach ( @issue_names ) {
-        my $i = eval { CIL::Issue->new_from_name($cil, $_) };
-        next unless defined $i;
-
-        $issue{$i->name} = $i;
-    }
-
-    if ( keys %issue ) {
-        msg( "Found actual issues: " . (join(' ', keys %issue)) );
-
-        # create the new comment
-        my $comment = CIL::Comment->new('tmpname');
-        $comment->Issue( '...' );
-        $comment->CreatedBy( $from );
-        $comment->Inserted( $date );
-        # $comment->Updated( $date );
-        $comment->Description( $body );
-
-        # display
-        display_comment($cil, $comment);
-
-        # found at least one issue, so this might be a comment
-        my $issue;
-        if ( keys %issue == 1 ) {
-            $issue = (values %issue)[0];
-        }
-        else {
-            my $ans = ans('To which issue would you like to add this comment: ');
-
-            # ToDo: decide whether we let them choose an arbitrary issue, for
-            # now quit unless they choose one from the list
-            return unless exists $issue{$ans};
-
-            # got a valid issue_name, so set the parent name
-            $issue = $issue{$ans};
-        }
-
-        # set the parent issue
-        $comment->Issue( $issue->name );
-
-        add_comment_loop($cil, undef, $issue, $comment);
-    }
-    else {
-        msg("Couldn't find reference to any issues in the email.");
-
-        # no issue found so make up the issue first
-        my $issue = CIL::Issue->new('tmpname');
-        $issue->Summary( $subject );
-        $issue->Status( 'New' );
-        $issue->CreatedBy( $from );
-        $issue->AssignedTo( user($cil) );
-        $issue->Inserted( $date );
-        $issue->Updated( $date );
-        $issue->Description( $body );
-
-        # display
-        display_issue_full($cil, $issue);
-
-        # then ask if the user would like to add it
-        msg("Couldn't find any likely issues, so this might be a new one.");
-        my $ans = ans('Would you like to add this message as an issue shown above (y/n): ');
-        return unless $ans eq 'y';
-
-        add_issue_loop($cil, undef, $issue);
-    }
-}
-
-sub cmd_depends_on {
-    my ($cil, undef, $issue_name, $depends_name) = @_;
-
-    my $issue = load_issue_fuzzy($cil, $issue_name);
-    my $depends = load_issue_fuzzy($cil, $depends_name);
-
-    $issue->add_depends_on( $depends->name );
-    $depends->add_precedes( $issue->name );
-
-    $issue->save($cil);
-    $depends->save($cil);
-}
-
-sub cmd_precedes {
-    my ($cil, undef, $issue_name, $depends_name) = @_;
-
-    # switch them round and call 'DependsOn'
-    cmd_depends_on($cil, undef, $depends_name, $issue_name);
-}
-
-## ----------------------------------------------------------------------------
-# helpers
-
-sub load_issue_fuzzy {
-    my ($cil, $partial_name) = @_;
-
-    my $issues = $cil->list_issues_fuzzy( $partial_name );
-    unless ( defined $issues ) {
-        fatal("Couldn't find any issues using '$partial_name'");
-    }
-
-    if ( @$issues > 1 ) {
-        fatal('found multiple issues which match that name: ' . join(' ', map { $_->{name} } @$issues));
-    }
-
-    my $issue_name = $issues->[0]->{name};
-    my $issue = CIL::Issue->new_from_name($cil, $issue_name);
-    unless ( defined $issue ) {
-        fatal("Couldn't load issue '$issue_name'");
-    }
-
-    return $issue;
-}
-
-sub load_comment_fuzzy {
-    my ($cil, $partial_name) = @_;
-
-    my $comments = $cil->list_comments_fuzzy( $partial_name );
-    unless ( defined $comments ) {
-        fatal("Couldn't find any comments using '$partial_name'");
-    }
-
-    if ( @$comments > 1 ) {
-        fatal('found multiple comments which match that name: ' . join(' ', map { $_->{name} } @$comments));
-    }
-
-    my $comment_name = $comments->[0]->{name};
-    my $comment = CIL::comment->new_from_name($cil, $comment_name);
-    unless ( defined $comment ) {
-        fatal("Couldn't load comment '$comment_name'");
-    }
-
-    return $comment;
-}
-
-sub load_attachment_fuzzy {
-    my ($cil, $partial_name) = @_;
-
-    my $attachments = $cil->list_attachments_fuzzy( $partial_name );
-    unless ( defined $attachments ) {
-        fatal("Couldn't find any attachments using '$partial_name'");
-    }
-
-    if ( @$attachments > 1 ) {
-        fatal('found multiple attachments which match that name: ' . join(' ', map { $_->{name} } @$attachments));
-    }
-
-    my $attachment_name = $attachments->[0]->{name};
-    my $attachment = CIL::Attachment->new_from_name($cil, $attachment_name);
-    unless ( defined $attachment ) {
-        fatal("Couldn't load attachment '$partial_name'");
-    }
-
-    return $attachment;
-}
-
-sub ans {
-    my ($msg) = @_;
-    print $msg;
-    my $ans = <STDIN>;
-    chomp $ans;
-    print "\n";
-    return $ans;
-}
-
-sub add_issue_loop {
-    my ($cil, undef, $issue) = @_;
-
-    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 };
-            $edit = ans('Would you like to re-edit (y/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 = time . $issue->Inserted . $issue->Summary . $issue->Description;
-    $issue->set_name( substr(md5_hex($unique_str), 0, 8) );
-    $issue->save($cil);
-
-    # should probably be run from with $cil
-    $cil->run_hook('issue_post_save', $issue);
-
-    display_issue($cil, $issue);
-
-    return $issue;
-}
-
-sub add_comment_loop {
-    my ($cil, undef, $issue, $comment) = @_;
-
-    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 };
-            $edit = ans('Would you like to re-edit (y/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 = time . $comment->Inserted . $issue->Description;
-    $comment->set_name( substr(md5_hex($unique_str), 0, 8) );
-
-    # finally, save it
-    $comment->save($cil);
-
-    # add the comment to the issue, update it's timestamp and save it out
-    $issue->add_comment( $comment );
-    $issue->save($cil);
-    display_issue_full($cil, $issue);
-
-    return $comment;
-}
-
-sub print_fsck_errors {
-    my ($entity, $errors) = @_;
-    return unless keys %$errors;
-
-    separator();
-    foreach my $issue_name ( keys %$errors ) {
-        title( "$entity $issue_name ");
-        foreach my $error ( @{$errors->{$issue_name}} ) {
-            msg("* $error");
-        }
-    }
-}
-
 ## ----------------------------------------------------------------------------
 # hooks
 
 # none yet
 
-## ----------------------------------------------------------------------------
-# input/output
-
-sub display_issue {
-    my ($cil, $issue) = @_;
-
-    separator();
-    title( 'Issue ' . $issue->name() );
-    field( 'Summary', $issue->Summary() );
-    field( 'Status', $issue->Status() );
-    field( 'CreatedBy', $issue->CreatedBy() );
-    field( 'AssignedTo', $issue->AssignedTo() );
-    field( 'Label', $_ )
-        foreach sort @{$issue->LabelList()};
-    field( 'Comment', $_ )
-        foreach sort @{$issue->CommentList()};
-    field( 'Attachment', $_ )
-        foreach sort @{$issue->AttachmentList()};
-    field( 'DependsOn', $_ )
-        foreach sort @{$issue->DependsOnList()};
-    field( 'Precedes', $_ )
-        foreach sort @{$issue->PrecedesList()};
-    field( 'Inserted', $issue->Inserted() );
-    field( 'Updated', $issue->Inserted() );
-    text('Description', $issue->Description());
-    separator();
-}
-
-sub display_issue_full {
-    my ($cil, $issue) = @_;
-
-    separator();
-    title( 'Issue ' . $issue->name() );
-    field( 'Summary', $issue->Summary() );
-    field( 'Status', $issue->Status() );
-    field( 'CreatedBy', $issue->CreatedBy() );
-    field( 'AssignedTo', $issue->AssignedTo() );
-    field( 'Label', $_ )
-        foreach sort @{$issue->Label()};
-    field( 'DependsOn', $_ )
-        foreach sort @{$issue->DependsOnList()};
-    field( 'Precedes', $_ )
-        foreach sort @{$issue->PrecedesList()};
-    field( 'Inserted', $issue->Inserted() );
-    field( 'Updated', $issue->Inserted() );
-    text('Description', $issue->Description());
-
-    my $comments = $cil->get_comments_for( $issue );
-    foreach my $comment ( @$comments ) {
-        display_comment( $cil, $comment );
-    }
-
-    my $attachments = $cil->get_attachments_for( $issue );
-    foreach my $attachment ( @$attachments ) {
-        display_attachment( $cil, $attachment );
-        msg();
-    }
-
-    separator();
-}
-
-sub display_comment {
-    my ($cil, $comment) = @_;
-
-    title( 'Comment ' . $comment->name() );
-    field( 'CreatedBy', $comment->CreatedBy() );
-    field( 'Inserted', $comment->Inserted() );
-    field( 'Updated', $comment->Inserted() );
-    text('Description', $comment->Description());
-}
-
-sub display_attachment {
-    my ($cil, $attachment) = @_;
-
-    title( 'Attachment ' . $attachment->name() );
-    field( 'Filename', $attachment->Filename() );
-    field( 'CreatedBy', $attachment->CreatedBy() );
-    field( 'Inserted', $attachment->Inserted() );
-    field( 'Updated', $attachment->Inserted() );
-}
-
-sub user {
-    my ($cil) = @_;
-    return $cil->UserName . ' <' . $cil->UserEmail . '>';
-}
-
 ## ----------------------------------------------------------------------------
 # helper functions for this command line tool
 
@@ -933,25 +143,6 @@ sub get_options {
     return $args;
 }
 
-sub text {
-    my ($field, $value) = @_;
-    msg "";
-    msg($value);
-    msg "";
-}
-
-sub err {
-    print STDERR ( defined $_[0] ? $_[0] : '' );
-    print STDERR "\n";
-}
-
-sub fatal {
-    my ($msg) = @_;
-    chomp $msg;
-    print STDERR $msg, "\n";
-    exit 2;
-}
-
 ## ----------------------------------------------------------------------------
 # program info
 
index 29a144608839ba93b8132359df579c3209cbe4cb..327adc69034dcae70c7a655f39c9d5d4c11faa3d 100644 (file)
@@ -318,8 +318,6 @@ sub register_hook {
 sub run_hook {
     my ($self, $hook_name, @rest) = @_;
 
-    print "s=$self, hn=$hook_name, r=@rest\n";
-
     unless ( defined $allowed->{hook}{$hook_name} ) {
         croak "hook '$hook_name' not allowed";
     }
@@ -388,132 +386,5 @@ sub UserEmail {
 }
 
 ## ----------------------------------------------------------------------------
-
-#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-sub check_paths {
-    my ($cil) = @_;
-
-    # make sure an issue directory is available
-    unless ( $cil->dir_exists($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;
-
-    # check that they aren't filtering on both --assigned-to and --is-mine
-    if ( defined $args->{a} and defined $args->{'is-mine'} ) {
-        fatal("the --assigned-to and --is-mine filters are mutually exclusive");
-    }
-
-    # 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->LabelList};
-        }
-        @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'
-    $args->{a} = $cil->UserEmail
-        if defined $args->{'is-mine'};
-    if ( defined $args->{a} ) {
-        @new_issues = grep { $args->{a} eq $_->assigned_to_email } @new_issues;
-    }
-
-    return \@new_issues;
-}
-
-#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-sub separator {
-    msg('=' x 79);
-}
-
-#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-sub msg {
-    print ( defined $_[0] ? $_[0] : '' );
-    print "\n";
-}
-
-#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-sub display_issue_summary {
-    my ($cil, $issue) = @_;
-
-    my $msg = $issue->name();
-    $msg .= "\t";
-    $msg .= $issue->Status();
-    $msg .= "\t";
-    $msg .= $issue->Summary();
-
-    msg($msg);
-}
-#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-sub display_issue_headers {
-    my ($cil, $issue) = @_;
-
-    title( 'Issue ' . $issue->name() );
-    field( 'Summary', $issue->Summary() );
-    field( 'CreatedBy', $issue->CreatedBy() );
-    field( 'AssignedTo', $issue->AssignedTo() );
-    field( 'Inserted', $issue->Inserted() );
-    field( 'Status', $issue->Status() );
-    field( 'Labels', join(' ', @{$issue->LabelList()}) );
-    field( 'DependsOn', join(' ', @{$issue->DependsOnList()}) );
-    field( 'Precedes', join(' ', @{$issue->PrecedesList()}) );
-}
-
-#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-sub title {
-    my ($title) = @_;
-    my $msg = "--- $title ";
-    $msg .= '-' x (74 - length($title));
-    msg($msg);
-}
-
-#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-sub field {
-    my ($field, $value) = @_;
-    my $msg = "$field";
-    $msg .= " " x (12 - length($field));
-    msg("$msg: " . (defined $value ? $value : '') );
-}
-
 1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Add.pm b/lib/CIL/Command/Add.pm
new file mode 100644 (file)
index 0000000..c0a2d15
--- /dev/null
@@ -0,0 +1,51 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Add;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'add' }
+
+sub run {
+    my ($self, $cil, undef, @argv) = @_;
+
+    CIL::Utils->ensure_interactive();
+
+    my $user = CIL::Utils->user($cil);
+
+    my $issue = CIL::Issue->new('tmpname');
+    $issue->Summary( join ' ', @argv );
+    $issue->Status('New');
+    $issue->CreatedBy( $user );
+    $issue->Description("Description ...");
+
+    CIL::Utils->add_issue_loop($cil, undef, $issue);
+}
+
+1;
+
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Am.pm b/lib/CIL/Command/Am.pm
new file mode 100644 (file)
index 0000000..1fbd749
--- /dev/null
@@ -0,0 +1,131 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Am;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'am' }
+
+sub run {
+    my ($self, $cil, undef, $email_filename) = @_;
+
+    unless ( -f $email_filename ) {
+        $cil->fatal("couldn't load email '$email_filename'");
+    }
+
+    my $msg_text = read_file($email_filename);
+
+    my $email = Email::Simple->new($msg_text);
+    unless ( defined $email ) {
+        $cil->fatal("email file '$email_filename' didn't look like an email");
+    }
+
+    # extract some fields
+    my $subject = $email->header('Subject');
+    my $from    = $email->header('From');
+    my $date    = find_date($email)->datetime;
+    my $body    = $email->body;
+
+    # see if we can find any issue names in either the subject or the body
+    my @issue_names;
+    foreach my $text ( $subject, $body ) {
+        my @new = ( $text =~ /\b\#?([0-9a-f]{8})\b/gxms );
+        push @issue_names, @new;
+    }
+
+    $cil->msg("Found possible issue names in email: ", ( join(' ', @issue_names) || '[none]' ));
+
+    my %issue;
+    foreach ( @issue_names ) {
+        my $i = eval { CIL::Issue->new_from_name($cil, $_) };
+        next unless defined $i;
+
+        $issue{$i->name} = $i;
+    }
+
+    if ( keys %issue ) {
+        $cil->msg( "Found actual issues: " . (join(' ', keys %issue)) );
+
+        # create the new comment
+        my $comment = CIL::Comment->new('tmpname');
+        $comment->Issue( '...' );
+        $comment->CreatedBy( $from );
+        $comment->Inserted( $date );
+        # $comment->Updated( $date );
+        $comment->Description( $body );
+
+        # display
+        CIL::Utils->display_comment($cil, $comment);
+
+        # found at least one issue, so this might be a comment
+        my $issue;
+        if ( keys %issue == 1 ) {
+            $issue = (values %issue)[0];
+        }
+        else {
+            my $ans = ans('To which issue would you like to add this comment: ');
+
+            # ToDo: decide whether we let them choose an arbitrary issue, for
+            # now quit unless they choose one from the list
+            return unless exists $issue{$ans};
+
+            # got a valid issue_name, so set the parent name
+            $issue = $issue{$ans};
+        }
+
+        # set the parent issue
+        $comment->Issue( $issue->name );
+
+        add_comment_loop($cil, undef, $issue, $comment);
+    }
+    else {
+        $cil->msg("Couldn't find reference to any issues in the email.");
+
+        # no issue found so make up the issue first
+        my $issue = CIL::Issue->new('tmpname');
+        $issue->Summary( $subject );
+        $issue->Status( 'New' );
+        $issue->CreatedBy( $from );
+        $issue->AssignedTo( user($cil) );
+        $issue->Inserted( $date );
+        $issue->Updated( $date );
+        $issue->Description( $body );
+
+        # display
+        CIL::Utils->display_issue_full($cil, $issue);
+
+        # then ask if the user would like to add it
+        $cil->msg("Couldn't find any likely issues, so this might be a new one.");
+        my $ans = ans('Would you like to add this message as an issue shown above (y/n): ');
+        return unless $ans eq 'y';
+
+        CIL::Utils->add_issue_loop($cil, undef, $issue);
+    }
+}
+
+1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Attach.pm b/lib/CIL/Command/Attach.pm
new file mode 100644 (file)
index 0000000..ed354c5
--- /dev/null
@@ -0,0 +1,85 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Attach;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'attach' }
+
+sub run {
+    my ($self, $cil, undef, $issue_name, $filename) = @_;
+
+    my $issue = load_issue_fuzzy( $cil, $issue_name );
+
+    # check to see if the file exists
+    unless ( -r $filename ) {
+        $cil->fatal("couldn't read file '$filename'");
+    }
+
+    my $basename = basename( $filename );
+    my $user = user($cil);
+
+    my $add_attachment_text = <<"EOF";
+Filename    : $basename
+CreatedBy   : $user
+
+File goes here ... this will be overwritten.
+EOF
+
+    # read in the new issue text
+    CIL::Utils->ensure_interactive();
+    my $fh = CIL::Utils->solicit( $add_attachment_text );
+
+    my $attachment = CIL::Attachment->new_from_fh( 'tmp', $fh );
+    unless ( defined $attachment ) {
+        $cil->fatal("could not create new attachment");
+    }
+
+    # now add the file itself
+    my $contents = read_file( $filename );
+    $attachment->set_file_contents( $contents );
+
+    # set the size
+    my ($size) = (stat($filename))[7];
+    $attachment->Size( $size );
+
+    # we've got the attachment, so let's name it
+    my $unique_str = time . $attachment->Inserted . $attachment->File;
+    $attachment->set_name( substr(md5_hex($unique_str), 0, 8) );
+
+    # finally, tell it who it's parent is and then save
+    $attachment->Issue( $issue->name );
+    $attachment->save($cil);
+
+    # add the comment to the issue, update it's timestamp and save it out
+    $issue->add_attachment( $attachment );
+    $issue->save($cil);
+    CIL::Utils->display_issue_full($cil, $issue);
+}
+
+1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Comment.pm b/lib/CIL/Command/Comment.pm
new file mode 100644 (file)
index 0000000..c7a8cbb
--- /dev/null
@@ -0,0 +1,50 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Comment;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'comment' }
+
+sub run {
+    my ($self, $cil, undef, $issue_name) = @_;
+
+    my $issue = load_issue_fuzzy( $cil, $issue_name );
+
+    CIL::Utils->ensure_interactive();
+
+    # create the new comment
+    my $comment = CIL::Comment->new('tmpname');
+    $comment->Issue( $issue->name );
+    $comment->CreatedBy( user($cil) );
+    $comment->Description("Description ...");
+
+    CIL::Utils->add_comment_loop($cil, undef, $issue, $comment);
+}
+
+1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/DependsOn.pm b/lib/CIL/Command/DependsOn.pm
new file mode 100644 (file)
index 0000000..11bcec0
--- /dev/null
@@ -0,0 +1,47 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::DependsOn;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'depends-on' }
+
+sub run {
+    my ($self, $cil, $args, $issue_name, $depends_name) = @_;
+
+    my $issue = CIL::Utils->load_issue_fuzzy($cil, $issue_name);
+    my $depends = CIL::Utils->load_issue_fuzzy($cil, $depends_name);
+
+    $issue->add_depends_on( $depends->name );
+    $depends->add_precedes( $issue->name );
+
+    $issue->save($cil);
+    $depends->save($cil);
+}
+
+1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Edit.pm b/lib/CIL/Command/Edit.pm
new file mode 100644 (file)
index 0000000..9c9cd89
--- /dev/null
@@ -0,0 +1,71 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Edit;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+my $y = 'y';
+
+## ----------------------------------------------------------------------------
+
+sub name { 'edit' }
+
+sub run {
+    my ($self, $cil, undef, $issue_name) = @_;
+
+    my $issue = CIL::Utils->load_issue_fuzzy( $cil, $issue_name );
+
+    CIL::Utils->ensure_interactive();
+
+    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 };
+            $edit = ans('Would you like to re-edit (y/n): ');
+        }
+    }
+
+    # if the issue is still invalid, they quit without correcting it
+    return unless $issue->is_valid( $cil );
+
+    # save it
+    $issue->save($cil);
+    CIL::Utils->display_issue($cil, $issue);
+}
+
+1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Extract.pm b/lib/CIL/Command/Extract.pm
new file mode 100644 (file)
index 0000000..80b158b
--- /dev/null
@@ -0,0 +1,44 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Extract;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'extract' }
+
+sub run {
+    my ($self, $cil, $args, $attachment_name) = @_;
+
+    my $attachment = CIL::Utils->load_attachment_fuzzy($cil, $attachment_name);
+
+    my $filename = $args->{f} || $attachment->Filename();
+    write_file( $filename, $attachment->as_binary );
+}
+
+1;
+
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Fsck.pm b/lib/CIL/Command/Fsck.pm
new file mode 100644 (file)
index 0000000..e362e3e
--- /dev/null
@@ -0,0 +1,187 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Fsck;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'fsck' }
+
+sub run {
+    my ($self, $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;
+    }
+
+    # ------
+    # issues
+    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->CommentList;
+            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->AttachmentList;
+            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";
+                }
+            }
+
+            # check that all it's 'DependsOn' are there and that they have this under 'Precedes'
+            my $depends_on = $i->DependsOnList;
+            foreach my $d ( @$depends_on ) {
+                # see if this issue exists at all
+                if ( exists $issue->{$d} ) {
+                    # check the 'Precedes' is this issue
+                    my %precedes = map { $_ => 1 } @{$issue->{$d}->PrecedesList};
+                    push @{$errors->{$name}}, "issue '$d' should precede '" . $i->name . "' but doesn't"
+                        unless exists $precedes{$i->name};
+                }
+                else {
+                    push @{$errors->{$name}}, "issue '$d' listed as a dependency of issue '" . $i->name . "' does not exist";
+                }
+            }
+
+            # check that all it's 'Precedes' are there and that they have this under 'DependsOn'
+            my $precedes = $i->PrecedesList;
+            foreach my $p ( @$precedes ) {
+                # see if this issue exists at all
+                if ( exists $issue->{$p} ) {
+                    # check the 'DependsOn' is this issue
+                    my %depends_on = map { $_ => 1 } @{$issue->{$p}->DependsOnList};
+                    push @{$errors->{$name}}, "issue '$p' should depend on '" . $i->name . "' but doesn't"
+                        unless exists $depends_on{$i->name};
+                }
+                else {
+                    push @{$errors->{$name}}, "issue '$p' listed as preceding 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 left
+    separator();
+}
+
+sub print_fsck_errors {
+    my ($entity, $errors) = @_;
+    return unless keys %$errors;
+
+    separator();
+    foreach my $issue_name ( keys %$errors ) {
+        title( "$entity $issue_name ");
+        foreach my $error ( @{$errors->{$issue_name}} ) {
+            msg("* $error");
+        }
+    }
+}
+
+1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Init.pm b/lib/CIL/Command/Init.pm
new file mode 100644 (file)
index 0000000..e7aa6b0
--- /dev/null
@@ -0,0 +1,111 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Init;
+
+use strict;
+use warnings;
+use File::Slurp qw(read_file write_file);
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'init' }
+
+sub run {
+    my ($self, $cil, $args) = @_;
+
+    my $path = $args->{p} || '.'; # default path is right here
+
+    # error if $path doesn't exist
+    unless ( -d $path ) {
+        CIL::Utils->fatal("path '$path' doesn't exist");
+    }
+
+    # error if issues/ already exists
+    my $issues_dir = "$path/issues";
+    if ( -d $issues_dir ) {
+        CIL::Utils->fatal("issues directory '$issues_dir' already exists, not initialising issues");
+    }
+
+    # error if .cil already exists
+    my $config = "$path/.cil";
+    if ( -f $config ) {
+        CIL::Utils->fatal("config file '$config' already exists, not initialising issues");
+    }
+
+    # try to create the issues/ dir
+    unless ( mkdir $issues_dir ) {
+        CIL::Utils->fatal("Couldn't create '$issues_dir' directory: $!");
+    }
+
+    # are we in a Git repository?
+    my $VCSconfig = '';
+    if ( -d '.git' ) {
+        $cil->msg( 'git repository detected, setting VCS accordingly' );
+        $VCSconfig = 'VCS: git';
+        my $vcs = CIL::VCS::Factory->new( 'Git' );
+    }
+
+    # create a .cil file here also
+    if ( $args->{bare} ) {
+        unless ( touch $config ) {
+            rmdir $issues_dir;
+            CIL::Utils->fatal("couldn't create a '$config' file");
+        }
+    }
+    else {
+        # write a default .cil file
+        write_file($config, <<"CONFIG");
+$VCSconfig
+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
+CONFIG
+    }
+
+    # add a README.txt so people know what this is about
+    unless ( -f "$issues_dir/README.txt" ) {
+        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
+README
+    }
+
+    # $path/issues/ and $path/.cil create correctly
+    CIL::Utils->msg("initialised empty issue list inside '$path/'");
+}
+
+1;
+
+## ----------------------------------------------------------------------------
index 98424663cabaeb16d311b441a42595926e61e525..cdb5b0364e2cb1ca75f7213dd316b8fa7fe11f46 100644 (file)
@@ -1,28 +1,55 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
 package CIL::Command::List;
 
 use strict;
 use warnings;
 
-use base 'CIL::Command';
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'list' }
 
 sub run {
     my ($self, $cil, $args) = @_;
 
-    $cil->check_paths;
+    CIL::Utils->check_paths($cil);
 
     # find all the issues
     my $issues = $cil->get_issues();
-    $issues = $cil->filter_issues( $issues, $args );
+    $issues = CIL::Utils->filter_issues( $cil, $issues, $args );
     if ( @$issues ) {
         foreach my $issue ( sort { $a->Inserted cmp $b->Inserted } @$issues ) {
-            $cil->separator();
-            $cil->display_issue_headers($issue);
+            CIL::Utils->separator();
+            CIL::Utils->display_issue_headers($issue);
         }
-        $cil->separator();
+        CIL::Utils->separator();
     }
     else {
-        $cil->msg('no issues found');
+        CIL::Utils->msg('no issues found');
     }
 }
 
-'end of package CIL::Command::List';
+1;
+
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Precedes.pm b/lib/CIL/Command/Precedes.pm
new file mode 100644 (file)
index 0000000..f28c020
--- /dev/null
@@ -0,0 +1,48 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Precedes;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'precedes' }
+
+sub run {
+    my ($self, $cil, $args, $issue_name, $precedes_name) = @_;
+
+    my $issue = CIL::Utils->load_issue_fuzzy($cil, $issue_name);
+    my $precedes = CIL::Utils->load_issue_fuzzy($cil, $precedes_name);
+
+    $issue->add_precedes( $precedes->name );
+    $precedes->add_depends_on( $issue->name );
+
+    $issue->save($cil);
+    $precedes->save($cil);
+}
+
+1;
+
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Show.pm b/lib/CIL/Command/Show.pm
new file mode 100644 (file)
index 0000000..865fa3a
--- /dev/null
@@ -0,0 +1,42 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Show;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'show' }
+
+sub run {
+    my ($self, $cil, undef, $issue_name) = @_;
+
+    # firstly, read the issue in
+    my $issue = CIL::Utils->load_issue_fuzzy( $cil, $issue_name );
+    CIL::Utils->display_issue_full($cil, $issue);
+}
+
+1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Status.pm b/lib/CIL/Command/Status.pm
new file mode 100644 (file)
index 0000000..b811ec9
--- /dev/null
@@ -0,0 +1,51 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Status;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'status' }
+
+sub run {
+    my ($self, $cil, undef, $issue_name, $status) = @_;
+
+    unless ( defined $status ) {
+        $cil->fatal("provide a status to set this issue to");
+    }
+
+    # firstly, read the issue in
+    my $issue = CIL::Utils->load_issue_fuzzy( $cil, $issue_name );
+
+    # set the status for this issue
+    $issue->Status( $status );
+    $issue->save($cil);
+
+    CIL::Utils->display_issue($cil, $issue);
+}
+
+1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Steal.pm b/lib/CIL/Command/Steal.pm
new file mode 100644 (file)
index 0000000..2d2a9bb
--- /dev/null
@@ -0,0 +1,47 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Steal;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'steal' }
+
+sub run {
+    my ($self, $cil, $args, $issue_name) = @_;
+
+    # firstly, read the issue in
+    my $issue = CIL::Utils->load_issue_fuzzy( $cil, $issue_name );
+
+    # set the AssignedTo for this issue to you (because you're stealing it)
+    $issue->AssignedTo( user($cil) );
+    $issue->save($cil);
+
+    CIL::Utils->display_issue($cil, $issue);
+}
+
+1;
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Summary.pm b/lib/CIL/Command/Summary.pm
new file mode 100644 (file)
index 0000000..f5a3ab6
--- /dev/null
@@ -0,0 +1,55 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Summary;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'summary' }
+
+sub run {
+    my ($self, $cil, $args) = @_;
+
+    CIL::Utils->check_paths($cil);
+
+    # find all the issues
+    my $issues = $cil->get_issues();
+    $issues = CIL::Utils->filter_issues( $cil, $issues, $args );
+    if ( @$issues ) {
+        CIL::Utils->separator();
+        foreach my $issue ( @$issues ) {
+            CIL::Utils->display_issue_summary($issue);
+        }
+        CIL::Utils->separator();
+    }
+    else {
+        CIL::Utils->msg('no issues found');
+    }
+}
+
+1;
+
+## ----------------------------------------------------------------------------
diff --git a/lib/CIL/Command/Track.pm b/lib/CIL/Command/Track.pm
new file mode 100644 (file)
index 0000000..fe851b5
--- /dev/null
@@ -0,0 +1,54 @@
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This file is part of 'cil'.
+#
+# cil is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+## ----------------------------------------------------------------------------
+
+package CIL::Command::Track;
+
+use strict;
+use warnings;
+
+use base qw(CIL::Command);
+
+## ----------------------------------------------------------------------------
+
+sub name { 'track' }
+
+sub run {
+    my ($self, $cil, undef, $issue_name) = @_;
+
+    CIL::Utils->fatal("the 'VCS' option in your .cil file is not set")
+        unless defined $cil->VCS;
+
+    CIL::Utils->fatal("the 'VCS' option currently only supports values of 'Git'")
+        unless $cil->VCS eq 'Git';
+
+    my $issue = CIL::Utils->load_issue_fuzzy($cil, $issue_name);
+
+    # add the issue to Git
+    my $issue_dir = $cil->IssueDir();
+    my @files;
+    push @files, "$issue_dir/i_" . $issue->name . '.cil';
+    push @files, map { "$issue_dir/c_${_}.cil" } @{ $issue->CommentList };
+    push @files, map { "$issue_dir/a_${_}.cil" } @{ $issue->AttachmentList };
+    CIL::Utils->msg("git add @files");
+}
+
+1;
+## ----------------------------------------------------------------------------
index 5394e61bafdad71e5ef89021ae84e15ec2c2cb63..a7401317709510b38a39195d7bd49605c66fe2ed 100644 (file)
@@ -29,11 +29,13 @@ use File::Temp qw(tempfile);
 use Email::Find;
 use POSIX qw(getpgrp tcgetpgrp);
 use Fcntl qw(:DEFAULT :flock);
+use Digest::MD5 qw(md5_hex);
 
 ## ----------------------------------------------------------------------------
 # setup some globals
 
 my $editor = $ENV{EDITOR} || 'vi';
+my $y = 'y';
 
 ## ----------------------------------------------------------------------------
 
@@ -124,6 +126,9 @@ sub write_cil_file {
     write_file($filename, $lines);
 }
 
+## ----------------------------------------------------------------------------
+# input
+
 # this method based on Term::CallEditor(v0.11)'s solicit method
 # original: Copyright 2004 by Jeremy Mates
 # copied under the terms of the GPL
@@ -184,6 +189,379 @@ sub ensure_interactive {
     return;
 }
 
+sub add_issue_loop {
+    my ($class, $cil, undef, $issue) = @_;
+
+    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 = $class->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 {
+            $class->msg($_) foreach @{ $issue->errors };
+            $edit = ans('Would you like to re-edit (y/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 = time . $issue->Inserted . $issue->Summary . $issue->Description;
+    $issue->set_name( substr(md5_hex($unique_str), 0, 8) );
+    $issue->save($cil);
+
+    # should probably be run from with $cil
+    $cil->run_hook('issue_post_save', $issue);
+
+    $class->display_issue($cil, $issue);
+
+    return $issue;
+}
+
+sub add_comment_loop {
+    my ($class, $cil, undef, $issue, $comment) = @_;
+
+    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 {
+            $class->msg($_) foreach @{ $issue->errors };
+            $edit = $class->ans('Would you like to re-edit (y/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 = time . $comment->Inserted . $issue->Description;
+    $comment->set_name( substr(md5_hex($unique_str), 0, 8) );
+
+    # finally, save it
+    $comment->save($cil);
+
+    # add the comment to the issue, update it's timestamp and save it out
+    $issue->add_comment( $comment );
+    $issue->save($cil);
+    $class->display_issue_full($cil, $issue);
+
+    return $comment;
+}
+
+## ----------------------------------------------------------------------------
+# loading
+
+sub load_issue_fuzzy {
+    my ($class, $cil, $partial_name) = @_;
+
+    my $issues = $cil->list_issues_fuzzy( $partial_name );
+    unless ( defined $issues ) {
+        $class->fatal("Couldn't find any issues using '$partial_name'");
+    }
+
+    if ( @$issues > 1 ) {
+        $class->fatal('found multiple issues which match that name: ' . join(' ', map { $_->{name} } @$issues));
+    }
+
+    my $issue_name = $issues->[0]->{name};
+    my $issue = CIL::Issue->new_from_name($cil, $issue_name);
+    unless ( defined $issue ) {
+        $class->fatal("Couldn't load issue '$issue_name'");
+    }
+
+    return $issue;
+}
+
+sub load_comment_fuzzy {
+    my ($class, $cil, $partial_name) = @_;
+
+    my $comments = $cil->list_comments_fuzzy( $partial_name );
+    unless ( defined $comments ) {
+        $class->fatal("Couldn't find any comments using '$partial_name'");
+    }
+
+    if ( @$comments > 1 ) {
+        $class->fatal('found multiple comments which match that name: ' . join(' ', map { $_->{name} } @$comments));
+    }
+
+    my $comment_name = $comments->[0]->{name};
+    my $comment = CIL::comment->new_from_name($cil, $comment_name);
+    unless ( defined $comment ) {
+        $class->fatal("Couldn't load comment '$comment_name'");
+    }
+
+    return $comment;
+}
+
+sub load_attachment_fuzzy {
+    my ($class, $cil, $partial_name) = @_;
+
+    my $attachments = $cil->list_attachments_fuzzy( $partial_name );
+    unless ( defined $attachments ) {
+        $class->fatal("Couldn't find any attachments using '$partial_name'");
+    }
+
+    if ( @$attachments > 1 ) {
+        $class->fatal('found multiple attachments which match that name: ' . join(' ', map { $_->{name} } @$attachments));
+    }
+
+    my $attachment_name = $attachments->[0]->{name};
+    my $attachment = CIL::Attachment->new_from_name($cil, $attachment_name);
+    unless ( defined $attachment ) {
+        $class->fatal("Couldn't load attachment '$partial_name'");
+    }
+
+    return $attachment;
+}
+
+## ----------------------------------------------------------------------------
+# display
+
+sub display_issue {
+    my ($class, $cil, $issue) = @_;
+
+    $class->separator();
+    $class->title( 'Issue ' . $issue->name() );
+    $class->field( 'Summary', $issue->Summary() );
+    $class->field( 'Status', $issue->Status() );
+    $class->field( 'CreatedBy', $issue->CreatedBy() );
+    $class->field( 'AssignedTo', $issue->AssignedTo() );
+    $class->field( 'Label', $_ )
+        foreach sort @{$issue->LabelList()};
+    $class->field( 'Comment', $_ )
+        foreach sort @{$issue->CommentList()};
+    $class->field( 'Attachment', $_ )
+        foreach sort @{$issue->AttachmentList()};
+    $class->field( 'DependsOn', $_ )
+        foreach sort @{$issue->DependsOnList()};
+    $class->field( 'Precedes', $_ )
+        foreach sort @{$issue->PrecedesList()};
+    $class->field( 'Inserted', $issue->Inserted() );
+    $class->field( 'Updated', $issue->Inserted() );
+    $class->text('Description', $issue->Description());
+    $class->separator();
+}
+
+sub display_issue_full {
+    my ($class, $cil, $issue) = @_;
+
+    $class->separator();
+    $class->title( 'Issue ' . $issue->name() );
+    $class->field( 'Summary', $issue->Summary() );
+    $class->field( 'Status', $issue->Status() );
+    $class->field( 'CreatedBy', $issue->CreatedBy() );
+    $class->field( 'AssignedTo', $issue->AssignedTo() );
+    $class->field( 'Label', $_ )
+        foreach sort @{$issue->Label()};
+    $class->field( 'DependsOn', $_ )
+        foreach sort @{$issue->DependsOnList()};
+    $class->field( 'Precedes', $_ )
+        foreach sort @{$issue->PrecedesList()};
+    $class->field( 'Inserted', $issue->Inserted() );
+    $class->field( 'Updated', $issue->Inserted() );
+    $class->text('Description', $issue->Description());
+
+    my $comments = $cil->get_comments_for( $issue );
+    foreach my $comment ( @$comments ) {
+        $class->display_comment( $cil, $comment );
+    }
+
+    my $attachments = $cil->get_attachments_for( $issue );
+    foreach my $attachment ( @$attachments ) {
+        $class->display_attachment( $cil, $attachment );
+        $class->msg();
+    }
+
+    $class->separator();
+}
+
+sub display_comment {
+    my ($class, $cil, $comment) = @_;
+
+    $class->title( 'Comment ' . $comment->name() );
+    $class->field( 'CreatedBy', $comment->CreatedBy() );
+    $class->field( 'Inserted', $comment->Inserted() );
+    $class->field( 'Updated', $comment->Inserted() );
+    $class->text('Description', $comment->Description());
+}
+
+sub display_attachment {
+    my ($class, $cil, $attachment) = @_;
+
+    $class->title( 'Attachment ' . $attachment->name() );
+    $class->field( 'Filename', $attachment->Filename() );
+    $class->field( 'CreatedBy', $attachment->CreatedBy() );
+    $class->field( 'Inserted', $attachment->Inserted() );
+    $class->field( 'Updated', $attachment->Inserted() );
+}
+
+sub filter_issues {
+    my ($class, $cil, $issues, $args) = @_;
+
+    # don't filter if we haven't been given anything
+    return $issues unless defined $args;
+    return $issues unless %$args;
+
+    # check that they aren't filtering on both --assigned-to and --is-mine
+    if ( defined $args->{a} and defined $args->{'is-mine'} ) {
+        $class->fatal("the --assigned-to and --is-mine filters are mutually exclusive");
+    }
+
+    # 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->LabelList};
+        }
+        @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'
+    $args->{a} = $cil->UserEmail
+        if defined $args->{'is-mine'};
+    if ( defined $args->{a} ) {
+        @new_issues = grep { $args->{a} eq $_->assigned_to_email } @new_issues;
+    }
+
+    return \@new_issues;
+}
+
+sub separator {
+    my ($class) = @_;
+    $class->msg('=' x 79);
+}
+
+sub msg {
+    my ($class, $msg) = @_;
+    print ( defined $msg ? $msg : '' );
+    print "\n";
+}
+
+sub display_issue_summary {
+    my ($class, $issue) = @_;
+
+    my $msg = $issue->name();
+    $msg .= "\t";
+    $msg .= $issue->Status();
+    $msg .= "\t";
+    $msg .= $issue->Summary();
+
+    $class->msg($msg);
+}
+
+sub display_issue_headers {
+    my ($class, $issue) = @_;
+
+    # $class->title( 'Issue ' . $issue->name() );
+    $class->field( 'Summary', $issue->Summary() );
+    $class->field( 'CreatedBy', $issue->CreatedBy() );
+    $class->field( 'AssignedTo', $issue->AssignedTo() );
+    $class->field( 'Inserted', $issue->Inserted() );
+    $class->field( 'Status', $issue->Status() );
+    $class->field( 'Labels', join(' ', @{$issue->LabelList()}) );
+    $class->field( 'DependsOn', join(' ', @{$issue->DependsOnList()}) );
+    $class->field( 'Precedes', join(' ', @{$issue->PrecedesList()}) );
+}
+
+sub title {
+    my ($class, $title) = @_;
+    my $msg = "--- $title ";
+    $msg .= '-' x (74 - length($title));
+    $class->msg($msg);
+}
+
+sub field {
+    my ($class, $field, $value) = @_;
+    my $msg = "$field";
+    $msg .= " " x (12 - length($field));
+    $class->msg("$msg: " . (defined $value ? $value : '') );
+}
+
+sub text {
+    my ($class, $field, $value) = @_;
+    $class->msg();
+    $class->msg($value);
+    $class->msg();
+}
+
+## ----------------------------------------------------------------------------
+# system
+
+sub check_paths {
+    my ($class, $cil) = @_;
+
+    # make sure an issue directory is available
+    unless ( $cil->dir_exists($cil->IssueDir) ) {
+        $class->fatal("couldn't find '" . $cil->IssueDir . "' directory");
+    }
+}
+
+sub ans {
+    my ($msg) = @_;
+    print $msg;
+    my $ans = <STDIN>;
+    chomp $ans;
+    print "\n";
+    return $ans;
+}
+
+sub err {
+    my ($class, $msg) = @_;
+    print STDERR ( defined $msg ? $msg : '' );
+    print STDERR "\n";
+}
+
+sub fatal {
+    my ($class, $msg) = @_;
+    chomp $msg;
+    print STDERR $msg, "\n";
+    exit 2;
+}
+
+## ----------------------------------------------------------------------------
+# helpers
+
 sub extract_email_address {
     my ($class, $text) = @_;
 
@@ -199,6 +577,11 @@ sub extract_email_address {
     return $email_address;
 }
 
+sub user {
+    my ($class, $cil) = @_;
+    return $cil->UserName . ' <' . $cil->UserEmail . '>';
+}
+
 ## ----------------------------------------------------------------------------
 1;
 ## ----------------------------------------------------------------------------