summaryrefslogtreecommitdiff
path: root/bin/cil
diff options
context:
space:
mode:
Diffstat (limited to 'bin/cil')
-rwxr-xr-xbin/cil667
1 files changed, 667 insertions, 0 deletions
diff --git a/bin/cil b/bin/cil
new file mode 100755
index 0000000..d5a0af1
--- /dev/null
+++ b/bin/cil
@@ -0,0 +1,667 @@
+#!/usr/bin/perl
+## ----------------------------------------------------------------------------
+# cil is a Command line Issue List
+# Copyright (C) 2008 Andrew Chilton
+#
+# This program 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/>.
+## ----------------------------------------------------------------------------
+
+use strict;
+use warnings;
+
+use Data::Dumper;
+
+use Getopt::Mixed "nextOption";
+use Digest::MD5 qw(md5_hex);
+use File::Touch;
+use File::Glob ':glob';
+use File::Basename;
+use File::Slurp qw(read_file write_file);
+use CIL;
+use CIL::Issue;
+use CIL::Comment;
+use CIL::Attachment;
+
+## ----------------------------------------------------------------------------
+# constants
+
+use constant VERSION => '0.2.1';
+
+my @IN_OPTS = (
+ 'p=s', # p = path
+ 'path>p', # for 'add'
+ 'f=s', # f = filename
+ 'filename=f', # for 'extract'
+
+ 'help',
+ 'version',
+);
+
+my %BOOLEAN_ARGS = (
+ help => 1,
+ version => 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
+
+{
+ my $args = get_options(\@IN_OPTS, \%BOOLEAN_ARGS);
+
+ # do the version and help
+ if ( exists $args->{version} ) {
+ print "cil version ".VERSION."\n";
+ exit;
+ }
+
+ if ( exists $args->{help} ) {
+ usage();
+ exit;
+ }
+
+ # make sure that the command given is valid
+ Getopt::Mixed::abortMsg('specify a command')
+ if @ARGV == 0;
+
+ my $command = shift @ARGV;
+ no strict 'refs';
+ if ( not defined &{"cmd_$command"} ) {
+ Getopt::Mixed::abortMsg("'$command' is not a valid cil command.");
+ }
+
+ my $cil = CIL->new();
+
+ &{"cmd_$command"}($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: $!");
+ }
+
+ # create a .cil file here also
+ unless ( touch $config ) {
+ rmdir $issues_dir;
+ fatal("couldn't create a '$config' file");
+ }
+
+ # 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_list {
+ my ($cil) = @_;
+
+ check_paths($cil);
+
+ # find all the issues
+ my $issues = $cil->get_issues();
+ if ( @$issues ) {
+ foreach my $issue ( sort { $a->Inserted cmp $b->Inserted } @$issues ) {
+ separator();
+ display_issue_headers($cil, $issue);
+ }
+ separator();
+ }
+ else {
+ msg('no issues found');
+ }
+}
+
+sub cmd_summary {
+ my ($cil) = @_;
+
+ check_paths($cil);
+
+ # find all the issues
+ my $issues = $cil->get_issues();
+ 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 = CIL::Issue->new_from_name($cil, $issue_name);
+ unless ( defined $issue ) {
+ fatal("Couldn't load issue '$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 = CIL::Issue->new_from_name($cil, $issue_name);
+ unless ( defined $issue ) {
+ fatal("Couldn't load issue '$issue_name'");
+ }
+
+ # set the status for this issue
+ $issue->Status( $status );
+ $issue->save($cil);
+
+ display_issue($cil, $issue);
+}
+
+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 );
+
+ # we've got the issue, so let's name it
+ my $unique_str = $issue->Inserted . $issue->Summary . $issue->Description;
+ $issue->set_name( substr(md5_hex($unique_str), 0, 8) );
+ $issue->save($cil);
+ display_issue($cil, $issue);
+}
+
+sub cmd_edit {
+ my ($cil, undef, $issue_name) = @_;
+
+ my $issue = CIL::Issue->new_from_name($cil, $issue_name);
+ unless ( defined $issue ) {
+ 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)");
+ }
+
+ $issue_edited->save($cil);
+ display_issue($cil, $issue_edited);
+}
+
+sub cmd_comment {
+ my ($cil, undef, $issue_name) = @_;
+
+ my $issue = CIL::Issue->new_from_name($cil, $issue_name);
+ unless ( defined $issue ) {
+ 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");
+ }
+
+ # 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 );
+ $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);
+}
+
+sub cmd_attach {
+ my ($cil, undef, $issue_name, $filename) = @_;
+
+ my $issue = CIL::Issue->new_from_name($cil, $issue_name);
+ unless ( defined $issue ) {
+ fatal("couldn't load issue '$issue_name'");
+ }
+
+ # check to see if the file exists
+ unless ( -r $filename ) {
+ fatal("couldn't read file '$filename'");
+ }
+
+ my $basename = basename( $filename );
+
+ my $add_attachment_text = <<"EOF";
+Filename : $basename
+CreatedBy : $gan <$gae>
+
+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 = $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, undef, $attachment_name, $args) = @_;
+
+ my $attachment = CIL::Attachment->new_from_name($cil, $attachment_name);
+ unless ( defined $attachment ) {
+ fatal("Couldn't load attachment '$attachment_name'");
+ }
+
+ my $filename = $args->{f} || $attachment->Filename();
+ write_file( $filename, $attachment->as_binary );
+}
+
+## ----------------------------------------------------------------------------
+
+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");
+ }
+}
+
+## ----------------------------------------------------------------------------
+# input/output
+
+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->Label()}) );
+}
+
+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->Label()};
+ field( 'Comment', $_ )
+ foreach sort @{$issue->Comment()};
+ field( 'Attachment', $_ )
+ foreach sort @{$issue->Attachment()};
+ 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( 'Inserted', $issue->Inserted() );
+ field( 'Updated', $issue->Inserted() );
+ text('Description', $issue->Description());
+
+ my $comments = $cil->get_comments_for( $issue );
+ foreach my $comment ( @$comments ) {
+ title( 'Comment ' . $comment->name() );
+ field( 'CreatedBy', $comment->CreatedBy() );
+ field( 'Inserted', $comment->Inserted() );
+ field( 'Updated', $comment->Inserted() );
+ text('Description', $comment->Description());
+ }
+
+ my $attachments = $cil->get_attachments_for( $issue );
+ foreach my $attachment ( @$attachments ) {
+ title( 'Attachment ' . $attachment->name() );
+ field( 'Filename', $attachment->Filename() );
+ field( 'CreatedBy', $attachment->CreatedBy() );
+ field( 'Inserted', $attachment->Inserted() );
+ field( 'Updated', $attachment->Inserted() );
+ msg();
+ }
+
+ separator();
+}
+
+## ----------------------------------------------------------------------------
+# helper functions for this command line tool
+
+sub get_options {
+ my ($in_opts, $booleans) = @_;
+
+ my $args = {};
+ Getopt::Mixed::init( @$in_opts );
+ while( my($opt, $val) = nextOption() ) {
+ # if boolean, keep a count of how many there is only
+ if ( exists $booleans->{$opt} ) {
+ $args->{$opt}++;
+ next;
+ }
+ # normal 'string' value
+ if ( defined $args->{$opt} ) {
+ unless ( ref $args->{$opt} eq 'ARRAY' ) {
+ $args->{$opt} = [ $args->{$opt} ];
+ }
+ push @{$args->{$opt}}, $val;
+ }
+ else {
+ $args->{$opt} = $val;
+ }
+ }
+ Getopt::Mixed::cleanup();
+ return $args;
+}
+
+sub msg {
+ print ( defined $_[0] ? $_[0] : '' );
+ print "\n";
+}
+
+sub separator {
+ msg('=' x 79);
+}
+
+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 : '') );
+}
+
+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
+
+sub usage {
+ print <<"END_USAGE";
+Usage: $0 COMMAND [options]
+
+Commands:
+ init [--path=PATH]
+ add
+ summary
+ list
+ show ISSUE
+ status ISSUE NEW_STATUS
+ edit ISSUE
+ comment ISSUE
+ attach ISSUE FILENAME
+ extract ATTACHMENT [--filename=FILENAME]
+
+See <http://kapiti.geek.nz/software/cil.html> for further information.
+Report bugs to <andychilton -at- gmail -dot- com>.
+END_USAGE
+}
+
+## ----------------------------------------------------------------------------
+
+=head1 NAME
+
+cil - the command-line issue list
+
+=head1 SYNOPSIS
+
+ $ cil init
+ $ cil summary
+ $ cil list
+
+ $ cil add
+ ... added issue 'cafebabe' ...
+ $ cil show cafebabe
+ $ cil edit cafebabe
+ $ cil status cafebabe InProgress
+
+ $ cil comment cafebabe
+ ... added comment 'deadbeef' ...
+
+ $ cil attach cafebabe filename.txt
+ ... added attachment 'decaf7ea' ...
+
+ $ cil extract decaf7ea
+ $ cil extract decaf7ea --filename=other_filename.txt
+
+=head1 DESCRIPTION
+
+Cil is a small but useful command-line issue list. It saves issues, comments
+and attachments as local files which you can check in to your repository.
+
+=over
+
+=item init [--path=PATH]
+
+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
+
+Displays a one line summary for each issue.
+
+=item list
+
+Shows each issue with more information.
+
+=item add
+
+Adds an issues after you have edited the input.
+
+=item show ISSUE
+
+Shows the issue name with more detail.
+
+=item status ISSUE NEW_STATUS
+
+Shortcut so that you can set a new status on an issue without having to edit
+it.
+
+=item edit ISSUE
+
+Edits the issue. If it changes, set the updates time to now.
+
+=item comment ISSUE
+
+Adds a comment to an issues after you have edited the input.
+
+=item attach ISSUE FILENAME
+
+Adds that particular filename to an existing issue.
+
+=item extract ATTACHMENT [--filename=FILENAME]
+
+Extracts the file from the attachment number. If filename if given uses that,
+otherwise it will use the original one saved along with the attachment.
+
+=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
+
+* set where you want your issues (from a .cil file)
+
+* simple search first, proper search and indexing second
+
+=head1 AUTHOR
+
+Andrew Chilton <andychilton@gmail.com>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2008 by Andrew Chilton
+
+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/> or write to the Free
+Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+=cut
+## ----------------------------------------------------------------------------