#! /usr/bin/perl
# <one line to give a brief idea of what this does.>
# 
# Copyright 2006 (c)      Mathieu Roy <yeupou--gnu.org>
#
# This file is part of Savane.
# 
# Savane is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# 
# Savane 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 Affero General Public License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

##
## Should be called from SVN hooks/post-commit
## Like svnmailer, ciabot, etc, it takes several args
##   -t = Tree (repository root directory)
##   -r = Revision
##   -p = Project (unix group name)
##   
## It must be in setuid/setgid, otherwise Savane conffile cannot be accessed
##

our $dbd;

use strict;
use Savane;
use Getopt::Long;
use HTML::Entities;

my $repository;
my $revision;
my $unix_group_name;
my $debug;
my $getopt;


# get options
eval {
    $getopt = GetOptions("tree=s" => \$repository,
			 "revision=s" => \$revision,
			 "project=s" => \$unix_group_name,
			 "debug" => \$debug);
};


##########################
#
# Secure input
#
##########################


# Exit on missing args
die "Repository argument missing. Exiting" unless $repository;
die "Revision argument missing. Exiting" unless $revision;
die "Project argument missing. Exiting" unless $unix_group_name;

# Exit if the args looks weird.

# Repository must be a valid path
die "Repository not found. Exiting" unless -d $repository;

# Revision must be a number 
die "Weird revision number. Exiting" if $revision =~ /\D/;

# Project must be a valid group on the system
die "Group not found. Exiting" unless getgrnam($unix_group_name);

# Untaint variables to make sure we can use them later in exec()
$revision = $1 if $revision =~ /^(\d*)$/;
$repository = $1 if $repository =~ /^((\/|\-|\.|\w|\d)*)$/;

print "Secure input phase passed\n" if $debug;

##########################
#
# First, study the revision content, we must restrict SQL request as much
# as possible, so we should get all necessary info
#
# We must fork to run svn look, because otherwise this script wont run in
# setuid 
#
##########################

# Get the commit author
my $pid = open(SVNLOOK, '-|');
die "Cannot fork. Exit" unless defined $pid;
unless ($pid) {
    open(STDERR, ">&STDOUT")
        or die "Cannot redirect STDERR to STDOUT. Exiting";
    exec("svnlook", "author", $repository, "-r", $revision)
        or die "Cannot exec `@_'. Exiting";
}
my $author;
while (<SVNLOOK>) {
    $author = $_;
    # there should be only one line
    last;
}
close(SVNLOOK);
close(STDERR);
chomp($author);
# Exit if the commit was not made by a valid user on the system
die "User not found. Exiting" unless getpwnam($author);

print "Author found\n" if $debug;

# Get the commit message 
$pid = open(SVNLOOK, '-|');
die "Cannot fork. Exit" unless defined $pid;
unless ($pid) {
    open(STDERR, ">&STDOUT")
        or die "Cannot redirect STDERR to STDOUT. Exiting";
    exec("svnlook", "log", $repository, "-r", $revision)
        or die "Cannot exec `@_'. Exiting";
}

my $log;
my %items;
# Try to find items refs (for now, ignore recipes as they are very specific)
# The save refs must refer to the actual database name of the trackers, not
# the user friendly one (bugs and not bug, support and not sr)
my $limit = 0;
while (<SVNLOOK>) {
    while (/(^|\s|\W)(bugs?)\s{0,2}#(\d+)/ig)	{
	$items{"bug$3"} = "bugs:$3" unless $items{"bug$3"};
    }
    while (/(^|\s|\W)(support|sr)\s{0,2}#(\d+)/ig)	{
	$items{"sr$3"} = "support:$3" unless $items{"sr$3"};
    }
    while (/(^|\s|\W)(tasks?)\s{0,2}#(\d+)/ig)	{
	$items{"task$3"} = "task:$3" unless $items{"task$3"};
    }
    while (/(^|\s|\W)(patch)\s{0,2}#(\d+)/ig)	{
	$items{"patch$3"} = "patch:$3" unless $items{"patch$3"};
    }

    $log .= $_;
    
    # If the log is longer than 50 lines, stop
    $limit++;
    if ($limit > 50) {
	$log .= "\n[...]";
    }
}
close(SVNLOOK);
close(STDERR);

print "Log found\n" if $debug;


# If no items refs found, silently stop
exit unless scalar(%items);
# If too many items refs found, silently stop. Limit set to 10. Important to
# avoid going too hard on the SQL requests
exit if scalar(%items) > 10;


# # Get the affected files
# # DEACTIVATED: path can be very very long, broke the page layout.
# # Considering the frontend automatically add a link to the revision details
# # it is best to skip this part
# 
# $pid = open(SVNLOOK, '-|');
# die "Cannot fork. Exit" unless defined $pid;
# unless ($pid) {
#     open(STDERR, ">&STDOUT")
#         or die "Cannot redirect STDERR to STDOUT. Exiting";
#     exec("svnlook", "changed", $repository, "-r", $revision)
#         or die "Cannot exec `@_'. Exiting";
# }
#
# open(SVNLOOK, "svnlook changed $repository -r $revision |");
# my $changed;
# $limit = 0;
# while (<SVNLOOK>) {
#     # Add the markup to make it a list
#     $changed .= "* ".$_;  
#
#     # If there are more than 50 lines, stop
#     $limit++;
#     if ($limit > 50) {
# 	$changed .= "\n[...]\n";
#     }
# }
# close(SVNLOOK);
# # Add them as preamble of the changelog
# $log = "$changed\n$log";


# Clean up the log: HTML must be converted to the relevant entities
# (this is very important to avoid XSS absurdities)
chomp($log);
$log .= encode_entities($_);

print "Study revision content phase passed\n" if $debug;

##########################
#
# Now check if it is okay to do a commit:
#    item must belong to svn repository of the group
#
##########################

# Obtain savane gid and uid
my $group_id = GetGroupSettings($unix_group_name, "group_id");
my $user_id = GetUserSettings($author, "user_id");

# Check if these id were found. Normally, they should as we already checked
# previously that parameters where fine on the system. But if cost nothing
# to be sure.
die "Group not found in the database. Exiting" unless $group_id;
die "User not found in the database. Exiting" unless $user_id;

my $date = time();

# Now go after each item and check if we can add the comment.
# The current approach is simple in the code, but would probably not scale
# if we were to annotate more than 10 items at once. That why we have a limit
# set to 10, which should be a sensible limit for most commits.
while (my ($key,$val) = each(%items)) {
    my ($tracker, $item_id) = split(":", $val);

    print "Handle $tracker \#$item_id\n" if $debug;

    # Silently ignore items that does not belong to the group that owns the
    # SVN
    if (GetDBSettings($tracker, 
		      "bug_id='$item_id' AND group_id='$group_id'", 
		      "bug_id")) {
	# Usually, in comments, the field "old_value" is named details.
	# Here, we will use svn commit to distinguish this
	# kind of special comment easily
	# (mention svn in the string so in the interface, we will be sure
	# that links to svn will be relevant, in case someday we support
	# commits from another SCM)
	InsertDB($tracker."_history",
		 "bug_id,field_name,old_value,new_value,mod_by,date,type",
		 $dbd->quote($item_id).",".
		 $dbd->quote("svncommit").",".
		 $dbd->quote($log).",".
		 $dbd->quote($revision).",".
		 $dbd->quote($user_id).",".
		 $dbd->quote($date).",".
		 $dbd->quote("100"))
	    unless $debug;

	print $tracker."_history ".
	    "bug_id,field_name,old_value,new_value,mod_by,date,type ".
	    $dbd->quote($item_id).",".
	    $dbd->quote("svncommit").",".
	    $dbd->quote($log).",".
	    $dbd->quote($revision).",".
	    $dbd->quote($user_id).",".
	    $dbd->quote($date).",".
	    $dbd->quote("100")
	    if $debug;
		 
    }

}


# EOF
