[Rt-commit] rt branch 5.0/add-scrip-logging created. rt-5.0.4-64-g56bfc92e6d

BPS Git Server git at git.bestpractical.com
Mon Jul 31 22:58:00 UTC 2023


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt".

The branch, 5.0/add-scrip-logging has been created
        at  56bfc92e6d4bda9b08c7fc21b36acf29b2187490 (commit)

- Log -----------------------------------------------------------------
commit 56bfc92e6d4bda9b08c7fc21b36acf29b2187490
Author: Brad Embree <brad at bestpractical.com>
Date:   Mon Jul 31 14:59:08 2023 -0700

    Add tests

diff --git a/t/web/scrips.t b/t/web/scrips.t
index 1e9ee4e3fc..fff74c5a89 100644
--- a/t/web/scrips.t
+++ b/t/web/scrips.t
@@ -2,6 +2,7 @@ use strict;
 use warnings;
 
 use RT::Test tests => undef;
+use Test::Warn;
 
 RT->Config->Set( UseTransactionBatch => 1 );
 
@@ -296,4 +297,108 @@ note "apply scrip in different stage to different queues";
     is scalar @matches, 1, 'scrip mentioned only once';
 }
 
+note "test scrip logging";
+{
+    my $logdir = RT->Config->Get('LogDir') || File::Spec->catdir( $RT::VarPath, 'log' );
+    $logdir    = File::Spec->catdir( $logdir, 'scrips' );
+
+    my %test_scrips = (
+        'No Errors'          => [ 'return 1;',          'return 1;',          'return 1;' ],
+        'IsApplicable Error' => [ 'return $undefined;', 'return 1;',          'return 1;' ],
+        'Prepare Error'      => [ 'return 1;',          'return $undefined;', 'return 1;' ],
+        'Commit Error'       => [ 'return 1;',          'return 1;',          'return $undefined;' ],
+    );
+    my %test_scrip_logfile_should_exist = (
+        'No Errors'          => { IsApplicable => 0, Prepare => 0, Commit => 0, },
+        'IsApplicable Error' => { IsApplicable => 1, Prepare => 0, Commit => 0, },
+        'Prepare Error'      => { IsApplicable => 0, Prepare => 1, Commit => 0, },
+        'Commit Error'       => { IsApplicable => 0, Prepare => 0, Commit => 1, },
+    );
+
+    my %id_for_scrip;
+    foreach my $test_scrip ( sort keys %test_scrips  ) {
+        diag "Create Scrip (Test Scrip Logging - $test_scrip)" if $ENV{TEST_VERBOSE};
+        $m->follow_link_ok({id => 'admin-global-scrips-create'});
+        $m->form_name('CreateScrip');
+        $m->set_fields(
+            'Description'            => "Test Scrip Logging - $test_scrip",
+            'ScripCondition'         => 'User Defined',
+            'ScripAction'            => 'User Defined',
+            'Template'               => 'Blank',
+            'CustomIsApplicableCode' => $test_scrips{$test_scrip}->[0],
+            'CustomPrepareCode'      => $test_scrips{$test_scrip}->[1],
+            'CustomCommitCode'       => $test_scrips{$test_scrip}->[2],
+        );
+        $m->click('Create');
+        $m->content_like(qr{Scrip Created});
+
+        my ($sid) = ($m->content =~ /Modify scrip #(\d+)/);
+        ok $sid, "found scrip id on the page";
+
+        $id_for_scrip{$test_scrip} = $sid;
+    }
+
+    # creating a ticket should fire off all test scrips
+    diag "Create Ticket (Test Scrip Logging No Config)" if $ENV{TEST_VERBOSE};
+    warnings_like {
+        RT::Test->create_ticket(
+            Subject => 'Test Scrip Logging',
+            Content => 'stuff',
+            Queue   => 1,
+        );
+    } [ qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+      ];
+
+    # without any config specified there should be no log files
+    foreach my $test_scrip ( sort keys %id_for_scrip  ) {
+        foreach my $mode ( qw( IsApplicable Prepare Commit ) ) {
+            my $filename = 'scrip-' . $id_for_scrip{$test_scrip} . '-' . $mode . '.log';
+            my $fullpath = File::Spec->catfile( $logdir, $filename );
+
+            ok ! -e $fullpath, "Scrip log file '$filename' should not exist";
+        }
+    }
+
+    # now set config and create another ticket
+    # need to stop server, change config, restart server
+    # to avoid warning about changing config with running server
+    RT::Test->stop_server;
+    RT->Config->Set( LogScripsForUser => { root => 'warn', RT_System => 'warn' } );
+    ( $baseurl, $m ) = RT::Test->started_ok;
+    ok( $m->login(), 'logged in' );
+
+    diag "Create Ticket (Test Scrip Logging With Config)" if $ENV{TEST_VERBOSE};
+    warnings_like {
+        RT::Test->create_ticket(
+            Subject => 'Test Scrip Logging',
+            Content => 'stuff',
+            Queue   => 1,
+        );
+    } [ qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+        qr/Global symbol .* requires explicit package name/,
+      ];
+
+    foreach my $test_scrip ( sort keys %id_for_scrip  ) {
+        foreach my $mode ( qw( IsApplicable Prepare Commit ) ) {
+            my $filename = 'scrip-' . $id_for_scrip{$test_scrip} . '-' . $mode . '.log';
+            my $fullpath = File::Spec->catfile( $logdir, $filename );
+
+            if ( $test_scrip_logfile_should_exist{$test_scrip}->{$mode} ) {
+                ok -e $fullpath, "Scrip log file '$filename' should exist";
+            } else {
+                ok ! -e $fullpath, "Scrip log file '$filename' should not exist";
+            }
+        }
+    }
+}
+
 done_testing;

commit e1c1b2ab87b3596bf9a1621a79d177296c4f7e53
Author: Brad Embree <brad at bestpractical.com>
Date:   Mon Jul 31 14:58:22 2023 -0700

    Show Scrip errors for UserDefined code

diff --git a/share/html/Admin/Scrips/Elements/EditCustomCode b/share/html/Admin/Scrips/Elements/EditCustomCode
index 201a8c292f..345a67dbd9 100644
--- a/share/html/Admin/Scrips/Elements/EditCustomCode
+++ b/share/html/Admin/Scrips/Elements/EditCustomCode
@@ -65,6 +65,18 @@
     <textarea spellcheck="false" cols="80" class="form-control" rows="<% $lines %>" name="<% $method %>"><% $code %></textarea>
   </div>
 </div>
+
+% if ( $errors{$method} ) {
+<div class="form-row">
+  <div class="label col-2 labeltop">
+    <span style="color:red"><% loc('Logging') %></span>:
+  </div>
+  <div class="value col-9">
+    <textarea spellcheck="false" cols="80" rows="5" class="form-control" readonly><% $errors{$method} %></textarea>
+  </div>
+</div>
+% }
+
 % }
 
 </&>
@@ -79,4 +91,34 @@ my @list = (
 );
 
 my $min_lines = 10;
+
+my %errors = (
+    'CustomIsApplicableCode' => '',
+    'CustomPrepareCode'      => '',
+    'CustomCommitCode'       => '',
+);
+
+if ( $Scrip->id ) {
+    my @stages = ();
+    if ( $Scrip->ConditionObj->ExecModule eq 'UserDefined' ) {
+        push @stages, 'IsApplicable';
+    }
+    if ( $Scrip->ActionObj->ExecModule eq 'UserDefined' ) {
+        push @stages, 'Prepare', 'Commit';
+    }
+
+    my $logdir = RT->Config->Get('LogDir') || File::Spec->catdir( $RT::VarPath, 'log' );
+    $logdir    = File::Spec->catdir( $logdir, 'scrips' );
+    foreach my $stage ( @stages ) {
+        my $filename = File::Spec->catfile( $logdir, 'scrip-' . $Scrip->id . '-' .  $stage . '.log' );
+        if ( -e $filename ) {
+            if ( -s $filename ) {
+                local $/;
+                open ( my $f, '<:encoding(UTF-8)', $filename )
+                    or die "Cannot open initialdata file '$filename' for read: $@";
+                $errors{ 'Custom' . $stage . 'Code' } = <$f>;
+            }
+        }
+    }
+}
 </%INIT>

commit e4d083668d7c6b94e3c6d2c6875c537714aa7d05
Author: Brad Embree <brad at bestpractical.com>
Date:   Mon Jul 31 14:56:25 2023 -0700

    Add Logging tab to Scrip Admin menu

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 696d21b9af..f7bbc12d80 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -1567,6 +1567,7 @@ sub _BuildAdminMenu {
 
             $page->child( basics => title => loc('Basics') => path => "/Admin/Scrips/Modify.html?id=" . $id . $from_query_param );
             $page->child( 'applies-to' => title => loc('Applies to'), path => "/Admin/Scrips/Objects.html?id=" . $id . $from_query_param );
+            $page->child( 'logging' => title => loc('Logging'), path => "/Admin/Scrips/Logging.html?id=" . $id . $from_query_param );
         }
         elsif ( $request_path =~ m{^/Admin/Scrips/(index\.html)?$} ) {
             HTML::Mason::Commands::PageMenu->child( select => title => loc('Select') => path => "/Admin/Scrips/" );

commit f9b9936b7fa26ba28cd34ad5be6577202e301ee8
Author: Brad Embree <brad at bestpractical.com>
Date:   Mon Jul 31 14:55:57 2023 -0700

    Add Scrip Logging page

diff --git a/share/html/Admin/Scrips/Logging.html b/share/html/Admin/Scrips/Logging.html
new file mode 100644
index 0000000000..0e261a5df4
--- /dev/null
+++ b/share/html/Admin/Scrips/Logging.html
@@ -0,0 +1,113 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work 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, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Admin/Elements/Header, Title => loc("Logging for scrip #[_1]", $id) &>
+<& /Elements/Tabs &>
+
+<div class="mx-auto max-width-xl">
+
+<&| /Widgets/TitleBox, title => loc('Logging') &>
+
+<div class="form-row">
+  <div class="label col-3">
+    <span class="prev-icon-helper"><% loc('Condition') %>:</span>
+  </div>
+  <div class="value col-9">
+    <textarea cols="15" rows="5" name="Condition" class="form-control" readonly><% $errors{IsApplicable} %></textarea>
+  </div>
+</div>
+<div class="form-row">
+  <div class="label col-3">
+    <span class="prev-icon-helper"><% loc('Action preparation')%>:</span>
+  </div>
+  <div class="value col-9">
+    <textarea cols="15" rows="5" name="Prepare" class="form-control" readonly><% $errors{Prepare} %></textarea>
+  </div>
+</div>
+<div class="form-row">
+  <div class="label col-3">
+    <span class="prev-icon-helper"><% loc('Action commit') %>:</span>
+  </div>
+  <div class="value col-9">
+    <textarea cols="15" rows="5" name="Commit" class="form-control" readonly><% $errors{Commit} %></textarea>
+  </div>
+</div>
+
+</div>
+
+</&>
+
+<%ARGS>
+$id     => undef
+$From   => undef
+</%ARGS>
+<%INIT>
+my $scrip = RT::Scrip->new( $session{'CurrentUser'} );
+$scrip->Load( $id );
+Abort(loc("Couldn't load scrip #[_1]", $id))
+    unless $scrip->id;
+
+my %errors = (
+    'IsApplicable' => '',
+    'Prepare'      => '',
+    'Commit'       => '',
+);
+
+my $logdir = RT->Config->Get('LogDir') || File::Spec->catdir( $RT::VarPath, 'log' );
+$logdir    = File::Spec->catdir( $logdir, 'scrips' );
+foreach my $stage ( qw( IsApplicable Prepare Commit ) ) {
+    my $filename = File::Spec->catfile( $logdir, 'scrip-' . $scrip->id . '-' .  $stage . '.log' );
+    if ( -e $filename ) {
+        if ( -s $filename ) {
+            local $/;
+            open ( my $f, '<:encoding(UTF-8)', $filename )
+                or die "Cannot open initialdata file '$filename' for read: $@";
+            $errors{$stage} = <$f>;
+        }
+    }
+}
+</%INIT>

commit 294b3487d43a3f93ac831f43ca4901731eb51a2f
Author: Brad Embree <brad at bestpractical.com>
Date:   Mon Jul 31 14:54:39 2023 -0700

    Add HasLogs to Scrip AdminSearchResultFormat

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index fb805ba43a..d045d8993e 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -3977,7 +3977,7 @@ Set(%AdminSearchResultFormat,
     Scrips =>
         q{'<a href="__WebPath__/Admin/Scrips/Modify.html?id=__id____From__">__id__</a>/TITLE:#'}
         .q{,'<a href="__WebPath__/Admin/Scrips/Modify.html?id=__id____From__">__Description__</a>/TITLE:Description'}
-        .q{,__Condition__, __Action__, __Template__, __Disabled__},
+        .q{,__Condition__, __Action__, __Template__, __Disabled__,__HasLogs__},
 
     Templates =>
         q{'<a href="__WebPath__/__WebRequestPathDir__/Template.html?Queue=__QueueId__&Template=__id__">__id__</a>/TITLE:#'}

commit e7f5ef3869dc27a9bc3cba014e39305fd10f6541
Author: Brad Embree <brad at bestpractical.com>
Date:   Mon Jul 31 14:50:01 2023 -0700

    Add HasLogs column for Scrips

diff --git a/share/html/Elements/RT__Scrip/ColumnMap b/share/html/Elements/RT__Scrip/ColumnMap
index 1edac2a6d9..4e86be2748 100644
--- a/share/html/Elements/RT__Scrip/ColumnMap
+++ b/share/html/Elements/RT__Scrip/ColumnMap
@@ -181,6 +181,21 @@ my $COLUMN_MAP = {
             return $_[0]->loc( $os->FriendlyStage );
         },
     },
+    HasLogs => {
+        title => 'Logging', # loc
+        value => sub {
+            my $logdir = RT->Config->Get('LogDir') || File::Spec->catdir( $RT::VarPath, 'log' );
+            $logdir    = File::Spec->catdir( $logdir, 'scrips' );
+            foreach my $stage ( qw( IsApplicable Prepare Commit ) ) {
+                my $filename = File::Spec->catfile( $logdir, 'scrip-' . $_[0]->id . '-' .  $stage . '.log' );
+                if ( -e $filename && -s $filename ) {
+                    my $return = '<span style="color:red">' . $_[0]->loc('Has Log') . '</span>';
+                    return \$return;
+                }
+            }
+            return '';
+        },
+    },
 };
 
 </%ONCE>

commit b5f3ac0bf9a0ea6680cb6e679d6de164bd22d340
Author: Brad Embree <brad at bestpractical.com>
Date:   Thu Jul 13 19:16:27 2023 -0700

    Add logging for all scrip stages

diff --git a/lib/RT/Scrip.pm b/lib/RT/Scrip.pm
index ed56cafc61..f3042379c6 100644
--- a/lib/RT/Scrip.pm
+++ b/lib/RT/Scrip.pm
@@ -584,6 +584,9 @@ sub IsApplicable {
             return (undef);
         }
         my $ConditionObj = $self->ConditionObj;
+
+        $self->_AddFileLogger('IsApplicable');
+
         foreach my $TransactionObj ( @Transactions ) {
             # in TxnBatch stage we can select scrips that are not applicable to all txns
             my $txn_type = $TransactionObj->Type;
@@ -602,6 +605,7 @@ sub IsApplicable {
             }
         }
     };
+    $self->_RemoveFileLogger('IsApplicable');
 
     if ($@) {
         $RT::Logger->error( "Scrip IsApplicable " . $self->Id . " died. - " . $@ );
@@ -635,8 +639,12 @@ sub Prepare {
             TemplateObj    => $self->TemplateObj( $args{'TicketObj'}->Queue ),
         );
 
+        $self->_AddFileLogger('Prepare');
+
         $return = $self->ActionObj->Prepare();
     };
+    $self->_RemoveFileLogger('Prepare');
+
     if ($@) {
         $RT::Logger->error( "Scrip Prepare " . $self->Id . " died. - " . $@ );
         return (undef);
@@ -662,8 +670,11 @@ sub Commit {
 
     my $return;
     eval {
+        $self->_AddFileLogger('Commit');
+
         $return = $self->ActionObj->Commit();
     };
+    $self->_RemoveFileLogger('Commit');
 
 #Searchbuilder caching isn't perfectly coherent. got to reload the ticket object, since it
 # may have changed
@@ -680,9 +691,32 @@ sub Commit {
     return ($return);
 }
 
+sub _AddFileLogger {
+    my $self = shift;
+    my $mode = shift;
 
+    my $config       = RT->Config->Get('LogScripsForUser');
+    my $current_user = $HTML::Mason::Commands::session{CurrentUser} || $self->CurrentUser;
 
+    return unless $config;
+    return unless $current_user;
+    return unless $config->{ $current_user->Name };
 
+    RT::AddFileLogger(
+        filename  => 'scrip-' . $self->id . "-$mode.log",
+        log_level => $config->{ $current_user->Name },
+    );
+}
+
+sub _RemoveFileLogger {
+    my $self = shift;
+    my $mode = shift;
+
+    my $current_user = $HTML::Mason::Commands::session{CurrentUser} || $self->CurrentUser;
+    my $final_log    = "\nLog created on " . gmtime(time) . " for " . $current_user->Name;
+
+    RT::RemoveFileLogger( 'scrip-' . $self->id . "-$mode.log", $final_log );
+}
 
 # does an acl check and then passes off the call
 sub _Set {

commit 8a4a6fbe2334d901d9647f693ba11de5cc709729
Author: Brad Embree <brad at bestpractical.com>
Date:   Mon Jul 31 14:49:00 2023 -0700

    Add LogScripsForUser config option

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index adcd78594c..fb805ba43a 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -378,6 +378,22 @@ See the L<Log::Dispatch::Syslog> documentation for more information.
 
 Set(@LogToSyslogConf, ());
 
+=item C<$LogScripsForUser>
+
+Logging specifically for Scrips, enabled on a per user basis.
+
+If set, any Scrip that runs has logging enabled at the log level
+specified and if there is any log content it is displayed in the Scrip
+Admin interface.
+
+Map a User's Name to a log level to use for Scrip logging:
+
+    Set($LogScripsForUser, { User => 'warn' });
+
+=cut
+
+Set($LogScripsForUser, {});
+
 =back
 
 

commit 98b59574b1b05d65873052e286da32eba4cfcda6
Author: Brad Embree <brad at bestpractical.com>
Date:   Thu Jul 13 19:15:32 2023 -0700

    Add methods for adding and removing a logger

diff --git a/lib/RT.pm b/lib/RT.pm
index ea47927889..329e800f5a 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -412,6 +412,90 @@ sub InitSignalHandlers {
     };
 }
 
+sub AddFileLogger {
+    my %args  = (
+        log_level => 'warn',
+        @_
+    );
+
+    return unless $args{filename};
+
+    # return if there is a already a logger with this name
+    return if $RT::Logger->output( $args{filename} );
+
+    my $logdir   = RT->Config->Get('LogDir') || File::Spec->catdir( $VarPath, 'log' );
+    $logdir      = File::Spec->catdir( $logdir, 'scrips' );
+    my $filename = File::Spec->catfile( $logdir, $args{filename} );
+
+    unless ( -e $logdir ) {
+        mkdir $logdir;
+    }
+    unless ( -d $logdir && -w $logdir ) {
+        # localizing here would be hard when we don't have a current user yet
+        RT->Logger->error("Log dir '$logdir' is not writeable.");
+        return;
+    }
+
+    my $simple_cb = sub {
+        # if this code throw any warning we can get segfault
+        no warnings;
+        my %p = @_;
+
+        # skip Log::* stack frames
+        my $frame = 0;
+        $frame++ while caller($frame) && caller($frame) =~ /^Log::/;
+        my ($package, $filename, $line) = caller($frame);
+
+        # Encode to bytes, so we don't send wide characters
+        $p{message} = Encode::encode("UTF-8", $p{message});
+
+        $p{'message'} =~ s/(?:\r*\n)+$//;
+        return "[$$] [". gmtime(time) ."] [". $p{'level'} ."]: "
+            . $p{'message'} ." ($filename:$line)\n";
+    };
+
+    require Log::Dispatch::File;
+    $RT::Logger->add(
+        Log::Dispatch::File->new(
+            name      => $args{filename},
+            min_level => $args{log_level},
+            filename  => $filename,
+            mode      => 'write',
+            callbacks => [ $simple_cb ],
+        )
+    );
+
+    return 1;
+}
+
+sub RemoveFileLogger {
+    my $filename  = shift;
+    my $final_log = shift;
+
+    return unless $filename;
+
+    # return if there is not a logger with this name
+    return unless $RT::Logger->output($filename);
+
+    $RT::Logger->remove($filename);
+
+    # if the log file is empty delete it
+    my $logdir = RT->Config->Get('LogDir') || File::Spec->catdir( $VarPath, 'log' );
+    $logdir    = File::Spec->catdir( $logdir, 'scrips' );
+    $filename  = File::Spec->catfile( $logdir, $filename );
+
+    if ( ( -e $filename ) && ( -s $filename == 0 ) ) {
+        unlink $filename;
+    } elsif ( $final_log ) {
+        # add a final message with log details
+        if ( open my $fh, '>>', $filename ) {
+            print $fh $final_log;
+            close $fh;
+        } else {
+            RT->Logger->error("Cannot write to '$filename': $!");
+        }
+    }
+}
 
 sub CheckPerlRequirements {
     eval {require 5.010_001};

-----------------------------------------------------------------------


hooks/post-receive
-- 
rt


More information about the rt-commit mailing list