diff options
author | Francois Marier <francois@debian.org> | 2008-06-23 23:47:09 +1200 |
---|---|---|
committer | Francois Marier <francois@debian.org> | 2008-06-23 23:47:09 +1200 |
commit | 42280f662d3ce4affb00eb68a22a081dfb951395 (patch) | |
tree | 6fc148a570675adc70504d610cb2552b4ab3545e /bin |
Imported Upstream version 0.2.1upstream/0.2.1
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/cil | 667 |
1 files changed, 667 insertions, 0 deletions
@@ -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 +## ---------------------------------------------------------------------------- |