The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
###############################################################################
# Tweak Subversion log messages
# -----------------------------
# 
# It sure would be nice to be able to change the log messages on
# committed revisions of the Subversion repository via the web.  This
# is a quick attempt at making that happen.
#
# The idea here is that you visit this script at the web page.  With
# no action supplied, it will present a form asking for the revision
# of the log you wish to change.
#
# Upon submitting the form, it will come back with yet another form,
# which will:
#
#   - Display the current log message as static text.  
#   - Present a textarea for editing, initialized with the current
#     log message.
#
# The user can edit the message in the textarea, then submit that form,
# which will return a confirmation and show the new log message.
#
# ====================================================================
# Copyright (c) 2001-2003 CollabNet.  All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.  The terms
# are also available at http://subversion.tigris.org/license.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals.  For exact contribution history, see the revision
# history and logs, available at http://subversion.tigris.org/.
# ====================================================================

###############################################################################

use strict;
use CGI qw(:standard);

###############################################################################
# Configuration Section

my $gSvnlookCmd = '/usr/local/bin/svnlook';
my $gSvnadminCmd = '/usr/local/bin/svnadmin';
my $gReposPath = '/usr/www/repositories/svn';
my $gActionURL = './tweak-log.cgi';
my $gTempfilePrefix = '/tmp/tweak-cgi';
my $gHistoryFile = './TWEAKLOG';
my $gBypassRevpropHooks = 0; # set to 1 to bypass the repository hook system
my $gNumRecentCommits = 20;  # number of recent commits to show on init form
###############################################################################

my %gCGIValues = &doCGI( );
&main( );


#-----------------------------------------------------------------------------#
sub html_escape
# (log)
#-----------------------------------------------------------------------------#
{
  my $str = shift;
  $str =~ s/&/&/g;
  $str =~ s/>/>/g;
  $str =~ s/</&lt;/g;
  return $str;
}


#-----------------------------------------------------------------------------#
sub doCGI
# (void)
#-----------------------------------------------------------------------------#
{
    my $lCGI = new CGI;
    my @lFields = $lCGI->param;
    my $lField;
    my %lCGIData = ();

    foreach $lField ( @lFields )
    {
        $lCGIData{ uc $lField } = $lCGI->param( $lField );
    }
    return( %lCGIData );
}


#-----------------------------------------------------------------------------#
sub doError
# (error)
#-----------------------------------------------------------------------------#
{
    my $error = shift @_;

    print "<html><head><title>Tweak Log - Error</title></head>\n";
    print "<body><h1>ERROR</h1>\n<p>$error</p></body></html>\n";
    return;
}


#-----------------------------------------------------------------------------#
sub main
# (void)
#-----------------------------------------------------------------------------#
{
    # Print out HTTP headers.
    print "Content-type: text/html; charset=UTF-8\n\n";

    # Figure out what action to take.
    if( $gCGIValues{'ACTION'} =~ /fetch/i )
    {
	&doFetchLog();
    }
    elsif( $gCGIValues{'ACTION'} =~ /commit/i )
    {
	&doCommitLog();
    }
    else
    {
	&doInitialForm();
    }
    return;
}


#-----------------------------------------------------------------------------#
sub doInitialForm
# (void)
#-----------------------------------------------------------------------------#
{
    my $youngest = `$gSvnlookCmd youngest $gReposPath`;
    my $rev;
    my $oldest;
    
    print "<html>\n<head>\n<title>Tweak Log</title>\n</head>\n";
    print "<body>\n<form action=\"$gActionURL\" method=\"post\">\n";
    print "<a name=\"__top__\"></a>\n";
    print "<p>\n";
    print "Boy, I sure would like to modify that log message for \n";
    print "revision <input type=\"text\" name=\"rev\" value\"\">\n";
    print "<input type=\"submit\" name=\"action\" value=\"Fetch Log\">\n";
    print "</p></form>\n";
    print "<p>\n";
    print "For convenience, here are the most recent $gNumRecentCommits\n";
    print "commits (click the revision number to edit that revision's log):\n";
    print "</p>\n";
    chomp $youngest;
    $oldest = $youngest - $gNumRecentCommits + 1;
    $oldest = 1 if( $oldest < 1 );
    $rev = $youngest;
    while( $rev >= $oldest )
    {
        my @infolines = `$gSvnlookCmd info $gReposPath -r $rev`;
        my $author = shift @infolines;
        my $date = shift @infolines;
        my $log_size = shift @infolines;

        print "<hr />\n";
        print "<a href=\"$gActionURL?action=Fetch+Log&rev=$rev\">Revision $rev</a>:<br />\n";
        print "<i>Author: $author</i><br />\n";
        print "<i>Date: $date</i><br />\n";
        print "<i>Log: </i><br /><pre>\n";
        map {
            $_ = &html_escape ($_);
        } @infolines;
	print @infolines;
	print "</pre><br />\n";
	print "<a href=\"#__top__\">(back to top)</a>\n";
        $rev--;
    }
    print "</body></html>\n";
    return;
}


#-----------------------------------------------------------------------------#
sub isValidRev
# (rev)
#-----------------------------------------------------------------------------#
{
    my $youngest = `$gSvnlookCmd youngest $gReposPath`;
    my $rev = shift @_;

    if(not (( $youngest =~ /^\d+$/) and
	    ( $youngest > 0 )))
    {
	&doError( "Unable to determine youngest revision" );
	return 0;
    }
    if(not (( $rev =~ /^\d+$/) and
	    ( $rev <= $youngest )))
    {
	&doError( "'$rev' is not a valid revision number" );
	return 0;
    }
    return 1;
}


#-----------------------------------------------------------------------------#
sub doFetchLog
# (void)
#-----------------------------------------------------------------------------#
{
    my $rev = $gCGIValues{'REV'};
    my $log;
    my $escaped_log;   ## HTML-escaped version of $log

    # Make sure we've requested a valid revision.
    if( not &isValidRev( $rev ))
    {
	return;
    }
    
    # Fetch the log for that revision.
    $log = `$gSvnlookCmd log $gReposPath -r $rev`;

    $escaped_log = &html_escape ($log);

    # Display the form for editing the revision
    print "<html>\n<head>\n<title>Tweak Log - Log Edit</title>\n</head>\n";
    print "<body>\n";
    print "<h1>Editing Log Message for Revision $rev</h1>\n";
    print "<h2>Current log message:</h2>\n";
    print "<blockquote><hr /><pre>$escaped_log</pre><hr /></blockquote>\n";
    print "<p><font color=\"red\">\n";
    print "<i>Every change made is logged in <tt>${gHistoryFile}</tt>.\n";
    print "If you make a bogus\n";
    print "change, you can still recover the old message from there.</i>\n";
    print "</font></p>\n";
    print "<form action=\"$gActionURL\" method=\"post\">\n";
    print "<h2>New log message:</h2>\n";
    print "<blockquote>\n";
    print "<textarea cols=\"80\" rows=\"25\" wrap=\"off\" name=\"log\">\n";
    print $escaped_log;
    print "</textarea><br />\n";
    print "<input type=\"hidden\" name=\"rev\" value=\"$rev\">\n";
    print "<input type=\"submit\" name=\"action\" value=\"Commit Changes\">\n";
    print "</blockquote>\n";
    print "</form></body></html>\n";
    return;
}


#-----------------------------------------------------------------------------#
sub doCommitLog
# (void)
#-----------------------------------------------------------------------------#
{
    my $rev = $gCGIValues{'REV'};
    my $log = $gCGIValues{'LOG'};
    my $orig_log;
    my $tempfile = "$gTempfilePrefix.$$";

    # Make sure we are about to change a valid revision.
    if (not &isValidRev( $rev ))
    {
	return;
    }

    # Get the original log from the repository.
    $orig_log = `$gSvnlookCmd log $gReposPath -r $rev`;

    # If nothing was changed, go complain to the user (shame on him for
    # wasting our time like that!)
    if ($log eq $orig_log)
    {
	&doError ("Log message doesn't appear to have been edited.");
	return;
    }
    
    # Open a tempfile
    if (not (open( LOGFILE, "> $tempfile")))
    {
	&doError ("Unable to open temporary file.");
	return;
    }

    # Dump the new log into the tempfile (and close it)
    print LOGFILE $log;
    close LOGFILE;

    # Tell our history file what we're about to do.
    if ($gHistoryFile)
    {
        if (not (open (HISTORY, ">> $gHistoryFile")))
        {
            &doError ("Unable to open history file.");
            return;
        }
        print HISTORY "====================================================\n";
        print HISTORY "REVISION $rev WAS:\n";
        print HISTORY "----------------------------------------------------\n";
        print HISTORY $orig_log;
        print HISTORY "\n";
    }

    # Now, make the mods
    if ($gBypassRevpropHooks)
    {
        `$gSvnadminCmd setlog $gReposPath -r$rev $tempfile --bypass-hooks`;
    }
    else
    {
        `$gSvnadminCmd setlog $gReposPath -r$rev $tempfile`;
    }
    
    # ...and remove the tempfile.  It is, after all, temporary.
    unlink $tempfile;

    # Now, tell the history file what we did.
    if ($gHistoryFile)
    {
        print HISTORY "----------------------------------------------------\n";
        print HISTORY "REVISION $rev IS:\n";
        print HISTORY "----------------------------------------------------\n";
        print HISTORY $log;
        print HISTORY "\n";
        close HISTORY;
    }

    # Now, re-read that logfile
    $log = `$gSvnlookCmd log $gReposPath -r $rev`;
    $log = &html_escape ($log);

    print "<html>\n<head>\n<title>Tweak Log - Log Changed</title>\n</head>\n";
    print "<body>\n";
    print "<h1>Success!</h1>\n";
    print "<h2>New Log Message for Revision $rev</h2>\n";
    print "<blockquote><hr /><pre>$log</pre><hr /></blockquote>\n";
    print "</body></html>\n";
    return;
}