diff --git a/lib/Redmine/CLI.pm b/lib/Redmine/CLI.pm new file mode 100644 index 0000000000000000000000000000000000000000..f0b651ac70cebd5e03e67c66c2b35bb7c82c8989 --- /dev/null +++ b/lib/Redmine/CLI.pm @@ -0,0 +1,280 @@ + +=pod + +=head1 NAME + + REdmine::CLI + +=head1 DESCRIPTION + + experimental stuff for something like a Redmine command line interface + +=cut + +package Redmine::CLI; + +use strict; + +use Util::JSON; +use Util::Simple_CSV; +use Util::Matrix; + +use Redmine::Wrapper; + +use Data::Dumper; + +my $default_config_fnm= 'redmine.json'; +my @default_home_dirs= ('etc', undef, 'bin'); + +sub new +{ + my $class= shift; + + my $obj= + { + # defaults + 'cfg_stanza' => 'Redmine', + 'op_mode' => 'list', + 'project_name' => undef, + }; + + my @cfg_fnm= ( + $ENV{REDMINE_CONFIG}, + $default_config_fnm, + map { join ('/', $ENV{'HOME'}, $_, $default_config_fnm) } @default_home_dirs + ); + + while (@cfg_fnm) + { + my $f= shift (@cfg_fnm); + next unless (defined ($f)); + # print "NOTE: trying [$f] as config filen name\n"; + if (-f $f) + { + print "NOTE: picked [$f] as config filen name\n"; + $obj->{'cfg_fnm'}= $f; + last; + } + } + + bless $obj, $class; + + $obj->set (@_); + + $obj; +} + +sub set +{ + my $obj= shift; + my %par= @_; + + my %res; + foreach my $par (keys %par) + { + $res{$par}= $obj->{$par}; + $obj->{$par}= $par{$par}; + } + + (wantarray) ? %res : \%res; +} + +sub parse_args +{ + my $self= shift; + my @ARGV= @_; + + binmode (STDOUT, ':utf8'); + binmode (STDERR, ':utf8'); + + my @PARS; + my @extra_options; + + while (defined (my $arg= shift (@ARGV))) + { + # print __LINE__, " arg=[$arg]\n"; + + if ($arg eq '--') { push (@PARS, @ARGV); @ARGV= (); } + elsif ($arg =~ /^--(.+)/) + { + my ($opt, $val)= split ('=', $1, $2); + + if ($opt eq 'help') { usage(); } + elsif ($opt eq 'config') { $self->{cfg_fnm}= $val || shift (@ARGV); } + elsif ($opt eq 'stanza') { $self->{cfg_stanza}= $val || shift (@ARGV); } + elsif ($opt eq 'project') { $self->{project_name}= $val || shift (@ARGV); } + elsif ($opt eq 'show') { $self->{op_mode}= 'show'; } + elsif ($opt eq 'list') { $self->{op_mode}= 'list'; } + # TODO: allow extra arguments + else { usage(); } + } + elsif ($arg =~ /^-(.+)/) + { + foreach my $opt (split ('', $1)) + { + if ($opt eq 'h') { usage(); exit (0); } + # elsif ($opt eq 'x') { $x_flag= 1; } + else { usage(); } + } + } + else + { + push (@PARS, $arg); + } + } + + $self->{_pars}= \@PARS; + + 1; +} + +sub init +{ + my $self= shift; + + $self->{_cfg}= my $cfg= Util::JSON::read_json_file ($self->{cfg_fnm}); + return undef unless (defined ($cfg)); + + $self->{_rm_cfg}= my $rm_cfg= $cfg->{$self->{cfg_stanza}}; + + $self->{_rm_wrapper}= my $mRM= new Redmine::Wrapper ('cfg' => $rm_cfg); + + ($cfg, $mRM); +} + +sub main_part2 +{ + my $self= shift; + + # print __LINE__, " self: ", Dumper ($self); + + my ($mRM, $rm_cfg, $op_mode, $pars)= map { $self->{$_} } qw(_rm_wrapper _rm_cfg op_mode _pars); + unless (defined ($mRM)) + { + print "ATTN: Redmine::Wrapper not defined!\n"; + return undef; + } + + # print __LINE__, " mRM: ", Dumper ($mRM); + + print "op_mode=[$op_mode]\n"; + my $project_name= $self->{'project_name'} || $rm_cfg->{'project-name'}; + unless (defined ($project_name)) + { # TODO: look up project id in Redmine itself + print "ATTN: no project name found in configuration!\n"; + } + + my $tr_id_task= $mRM->get_tracker_id('Task'); + print "tr_id_task=[$tr_id_task]\n"; + + my $project_id= $rm_cfg->{project_ids}->{$project_name}; + unless (defined ($project_id)) + { # TODO: look up project id in Redmine itself + print "ATTN: no project_id found in config for project_name=[$project_name]\n"; + } + + if ($op_mode eq 'show') + { + my $rm= $mRM->attach(); + foreach my $ticket_number (@$pars) + { + Redmine::CLI::show_issue ($rm, $ticket_number); + } + } + elsif ($op_mode eq 'list') + { + my $rm= $mRM->attach(); + print "project_name=[$project_name]\n"; + Redmine::CLI::show_issues ($rm, $project_name); + } +} + +sub show_issues +{ + my $rm= shift; + my $proj_name= shift; + + my $proj= $rm->project($proj_name); + print "proj_name=[$proj_name] proj: ", Dumper ($proj); + + my $project_id= $proj->{'project'}->{'id'}; + # print "project_id=[$project_id]\n"; + + my (@data, @rows); + my $row_count= 0; + my @columns1= qw(id tracker status subject assigned_to updated_on); + my @columns2= qw(Issue Tracker Status Subject Assigned_to Updated_on); + + my $offset= 0; + while (1) + { + my $issues= $rm->issues( { project_id => $project_id, offset => $offset } ); + # print "issues: ", Dumper ($issues); + my ($i_off, $i_tc, $i_lim)= map { $issues->{$_} } qw(offset total_count limit); + printf ("offset=%d total_count=%d limit=%d\n", $i_off, $i_tc, $i_lim); + + foreach my $issue (@{$issues->{'issues'}}) + { + my ($rec_a, $rec_h)= filter1($issue, \@columns1); + # print "rec: ", main::Dumper ($rec); + push (@rows, $rec_a); + push (@data, $rec_h); + $row_count++; + } + + last if (($offset= $i_off + $i_lim) >= $i_tc); + } + + my $csv= new Util::Simple_CSV ('UTF8' => 1, verbose => 1); + $csv->define_columns (@columns1); + + # print "rows: ", Dumper (\@rows); + + # Util::Matrix::print (\@columns2, \@rows); + + $csv->{rows}= \@rows; + $csv->{data}= \@data; + $csv->{row_count}= $row_count; + # print "csv: ", Dumper ($csv); + # $csv->sort ('id', 0, 1); + $csv->sort ('subject', 0, 0); + + $csv->matrix_view (\@columns1); +} + +sub filter1 +{ + my $rec= shift; + my $fields= shift; + + my @dx= (); + my %dy= (); + + foreach my $field (@$fields) + { + my $x= $rec->{$field}; + $x= $x->{'name'} if (ref ($x) eq 'HASH'); + $dy{$field}= $x; + + push (@dx, $x); + } + (\@dx, \%dy); +} + +sub show_issue +{ + my $rm= shift; + my $ticket_number= shift; + + my $issues= $rm->issue( $ticket_number ); + print "issues: ", Dumper ($issues); +} + +sub usage +{ + system ('perldoc', __FILE__); + exit (0); +} + +1; + diff --git a/lib/Redmine/Wrapper.pm b/lib/Redmine/Wrapper.pm index 8b2f9d1eb56f417d1b826a004417624185e9c95b..7cb7c909290aef3914c1f4f0ccd1189a1582f205 100644 --- a/lib/Redmine/Wrapper.pm +++ b/lib/Redmine/Wrapper.pm @@ -5,6 +5,11 @@ use strict; use WebService::Redmine; +my %automapping= +( + 'project_ids' => 1, +); + sub new { my $class= shift; @@ -57,14 +62,32 @@ sub get_mapped_id # print "map_name=[$map_name] name=[$name] m=", main::Dumper ($m); my $id; + my $perform_lookup= 0; if (exists ($m->{$name})) { $id= $m->{$name}; + print "ATTN: no id known for $map_name=[$name]\n"; + + if (exists ($self->{automapping}) && $self->{automapping} >= 1) + { # TODO: add an *optional* lookup ... + $perform_lookup= 1; + } } else { - print "ATTN: no id known for $map_name=[$name]\n"; - # TODO: add a lookup ... + print "ATTN: no *map* named [$map_name] available\n"; + if (exists ($self->{automapping}) && $self->{automapping} >= 2 && exists ($automapping{$map_name})) + { # TODO: allow dynamically fetched maps, when the map name is valied, e.g. project_ids etc.. + if ($name eq 'project') + { + my $pi= $self->get_project_info ($name); + $perform_lookup= 1 if (defined ($pi)); + } + } + } + + if (!defined ($id) && $perform_lookup) + { } $id; @@ -86,6 +109,16 @@ sub get_project_id $self->get_mapped_id ('project_ids', $project_name); } +sub get_project_info +{ + my $self= shift; + my $name= shift; + + my $rm= $self->attach(); + my $proj= $rm->project( $name ); + print __LINE__, " get_project_info: name=[$name] proj: ", main::Dumper ($proj); +} + sub fixup_issue { my $self= shift; diff --git a/red.pl b/red.pl new file mode 100755 index 0000000000000000000000000000000000000000..8f675f590f784f1bf5a0721c89f9c2c1c81afc15 --- /dev/null +++ b/red.pl @@ -0,0 +1,22 @@ +#!/usr/bin/perl + +# NOTE: that should be redesigned ... + +use strict; + +use Data::Dumper; +$Data::Dumper::Indent= 1; + +use Redmine::CLI; + + my $rm_cli= new Redmine::CLI(); + # print __LINE__, " rm_cli: ", Dumper ($rm_cli); + + $rm_cli->parse_args(@ARGV); + + my ($cfg, $mRM)= $rm_cli->init(); + # print __LINE__, " rm_cli: ", Dumper ($rm_cli); + + $rm_cli->main_part2(); + +exit;