#! /usr/bin/env perl

# colorsvn
#
# based on colorgcc
#
# Requires the ANSIColor module from CPAN.
#
# Usage:
#
# 1.
# In a directory that occurs in your PATH _before_ the directory
# where svn lives, create a softlink to colorsvn:
#
#    svn -> colorsvn
#
# 2. 
# Set alias, e.g.:
#   alias svn='/usr/bin/colorsvn'
# 
# That's it. When "svn" is invoked, colorsvn is run instead.
#
# The default settings can be overridden with /etc/colorsvnrc or with ~/.colorsvnrc.
# See the colorsvnrc-sample for more information.
#
# Note:
#
# colorsvn will only emit color codes if:
# 
#    (1) tts STDOUT is a tty.
#    (2) the value of $TERM is listed in the "colortty" option.
#    (3) the svn command can be resolved, and configured for colorizing
#
# If colorsvn colorizes the output, svn's STDERR will be
# combined with STDOUT. Otherwise, colorsvn just passes the output from
# svn through without modification.
# 
# Copyright 2007 Valerij Klein <vklein@console-colors.de>
#
# Copyright 2002 Neil Stevens <neil@qualityassistant.com>
#
# Copyright 1999 Jamie Moyers <jmoyers@geeks.com>
#
# 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; version 2 of the License as published
# by the Free Software Foundation.
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, MA 02110-1301, USA.

use Term::ANSIColor;
use IPC::Open3;

use strict;
use warnings;

# script globals
our $svnPath;	                # path to svn 	
our %colors;		            # 
our %nocolor;                   # 
our %propcolors;                # properties colors
our %colortty;                  # terminal types listed in colortty-option
our %commands_to_colorize;      # commands to colorize
our $bolddir;                   # boldify directory

# only this colors are valid 
# prevent using errorenous colors
our @validcolors = (	"clear", 		 "black", 	   "on_black",
						"reset",     	 "red",        "on_red",
						"bold",          "green",      "on_green",
						"underline",     "yellow",     "on_yellow",
						"underscore",    "blue",       "on_blue",
						"blink",         "magenta",    "on_magenta",
						"reverse",       "cyan",       "on_cyan",
						"concealed",     "white",      "on_white");

# known svn commands						
our %commandmap = ( "add" => ["add"],
					"blame" => ["blame", "praise", "annotate", "ann"],
					"cat" => ["cat"],
					"changelist" => ["changelist"],
					"checkout" => ["checkout", "co"],
					"cleanup" => ["cleanup"],
					"commit" => ["commit", "ci"],
					"copy" => ["copy", "cp"],
					"delete" => ["delete", "del", "remove", "rm"],
					"diff" => ["diff", "di"],
					"export" => ["export"],
					"help" => ["help", "\?", "h"],
					"import" => ["import"],
					"info" => ["info"],
					"list" => ["list", "ls"],
					"lock" => ["lock"],
					"log" => ["log"],
					"merge" => ["merge"],
					"mkdir" => ["mkdir"],
					"move" => ["move", "mv", "rename", "ren"],
					"propdel" => ["propdel", "pdel", "pd"],
					"propedit" => ["propedit", "pedit", "pe"],
					"propget" => ["propget", "pget", "pg"],
					"proplist" => ["proplist", "plist", "pl"],
					"propset" => ["propset", "pset", "ps"],
					"resolved" => ["resolved"],
					"revert" => ["revert"],					
					"status" => ["status", "st", "stat"],
					"switch" => ["switch"],
					"unlock" => ["unlock"],
					"update" => ["update", "up"]					
					);
					
# never colorize this commands
our @preventcolor = ("commit", "help", "import", "lock", "propedit", "resolved", "revert", "switch", "unlock", "mkdir");
					
#
# set default values
sub initDefaults
{
	$svnPath = "/usr/bin/svn";

	$colortty{"dumb"} = "false";

	$colors{"P"} = "reset";
	$colors{"U"} = "reset";
	$colors{" "} = "reset";
	$colors{"C"} = "bold red";
	$colors{"M"} = "bold yellow";
	$colors{'G'} = "bold yellow";
	$colors{"A"} = "cyan";
	$colors{"R"} = "cyan";
	$colors{"D"} = "red";
	$colors{"I"} = "bold";
	$colors{"?"} = "reset";
	$colors{"!"} = "bold";
	$colors{"~"} = "bold red";
	$colors{"server"} = "bold green";
	$colors{"warning"} = "bold cyan";

	# Applies when only the properties changed
	$propcolors{"C"} = "bold red";
	$propcolors{"M"} = "yellow";
	
	$bolddir = 0;
}

#
#
# validate colors 
# on invalid color return "reset"
sub validateColor
{
	my ($color)=@_;
	my $retcolor = "";
	# split color string (may be "bold green on_red", or "on_white bold black", or just "black")
	my @coloropts = split(/\s+/, $color);
    # iterate through the list 
	foreach my $coloropt(@coloropts) {
		# grep for color in the list with known colors
		my @extr = grep(/^$coloropt$/, @validcolors);				
	
	    # if the color was not found it is an invalid color, set the color option on "reset" and last
		if ($#extr==-1)
		{
			$retcolor="reset";
			last;
		}
		else
		{
			$retcolor=$retcolor.$coloropt." ";
		}
	}

	$retcolor =~s/\s+$//;
	return $retcolor;
}

#
#
# read preferences from config file
sub loadPreferences
{
	# Usage: loadPreferences("filename");

	my($filename) = @_;

	open(PREFS, "<$filename") || return;

	while(<PREFS>)
	{
		next if (m/^\#.*/);          # It's a comment.
		next if (!m/(.*):\s*(.*)/);  # It's not of the form "foo: bar".

		my $option = $1;
		my $value = $2;
		$option =~ s/^\s+//;

		if ($option =~ /svn/)
		{
			$value =~ s/^\s+//;
			$value =~ s/\s+$//;
			$svnPath = $value;
		}
		elsif ($option eq "nocolor")
		{
			# The nocolor option lists terminal types, separated by
			# spaces, not to do color on.
			foreach my $termtype (split(/\s+/, $value))
			{
				$nocolor{$termtype} = "true";
			}
		}
		elsif ($option eq "colortty")
		{
			# The colortty option lists terminal types, separated by
			# spaces, to do color on.
			foreach my $termtype (split(/\s+/, $value))
			{
				$colortty{$termtype} = "true";
			}
		}		
		elsif ($option =~ /prop (.)/)
		{
		        # Property color
		        $propcolors{$1} = validateColor($value);
		}
		elsif ($option =~ /bolddir/) 
		{
		    # boldify directories
			$value =~ s/^\s+//;
			$value =~ s/\s+$//;
			
			if ($value eq "true") 
			{
				$bolddir = 1;
			}			
		}
		elsif ($option =~/commands/)
		{
			# The commands option lists svn commands, separated by
			# spaces, to do color on.
			foreach my $command (split(/\s+/, $value))
			{
				foreach my $svnopt (@{$commandmap{$command}}) {
					$commands_to_colorize{$svnopt} = 1;
				}
			}		
		}
		else
		{
			$value =~ s/^\s+//;
			$value =~ s/\s+$//;
			
			$colors{$option} = validateColor($value);
		}
	}
	close(PREFS);
}

#
# Main program
#

# Set up default values for colors and svn path.
initDefaults();

# Read the configuration file, if there is one.
my $configFile = "/etc/colorsvnrc";
my $alternateConfigFile = $ENV{"HOME"} . "/.colorsvnrc";

# File in HOME become higher prio
if (-f $alternateConfigFile) {
    $configFile = $alternateConfigFile;
}

# read config file
if (-f $configFile) {
    loadPreferences($configFile);
}

# Get the terminal type. 
my $terminal = $ENV{"TERM"} || "dumb";

my $commit = 1;
my $props = 0;
my $verbose = 0;
my $svncommand = "";
my $commresolved = 0;

# resolve svn command
RESOLVECOMM:
{
    foreach my $argument (@ARGV) {
        # option in form "--verbose, -h"
        if ($argument!~/^\-/) {
            # is the command known?
            foreach my $maincommand (keys %commandmap) {
                foreach my $command (@{$commandmap{$maincommand}}) {                    
                    if ($argument eq $command) {
                        $svncommand = $maincommand;
                        # if the command matched, last this block
                        $commresolved=1;
                        last RESOLVECOMM;
                    }
                }
            }
        }
    }
}

# check for colors for svn command
if ($svncommand ne "" && $commands_to_colorize{$svncommand} && !grep(/^$svncommand$/, @preventcolor))
{
	$commit = 0;
}

# If it's not in the list of terminal types to color, or if
# we're writing to something that's not a tty, don't do color,
# or unknown command
if (! -t STDOUT || $commit == 1 || !$colortty{$terminal} || !$commresolved )
{
	exec $svnPath, @ARGV
		or die("Couldn't exec");
}

-f $svnPath or die ("$svnPath not found, add svn=/full/path/to/svn to $configFile");

# Keep the pid of the svn process so we can get its return
# code and use that as our return code.
my $svn_pid = open3('<&STDIN', \*SVNOUT, \*SVNOUT, $svnPath, @ARGV);
my $svnName = $svnPath;
$svnName =~ s,.*/(.*)$,$1,;


# Colorize the output from the svn program.
while(<SVNOUT>)
{
	chomp;
	if (m/^ (.).+/ && $props) # Property changed only 
	{
	    print(color($propcolors{$1}), $_, color("reset"));
	}
	elsif (m/^(.)(M|C)?(\s+)(.+)/) # S filename
	{
		my $modifier = $1;

		if ($modifier ne "" && $colors{$modifier} && $colors{$modifier} ne "") {
			my $color = $colors{$modifier};
			# use bold for directory 
			if (-d $4 && $bolddir) {
				if ($color eq "reset") {
					$color="bold";
				} elsif ($color!~/^bold/) {
					$color="bold ".$color;
				}
			}
			
			print(color($color), $_, color("reset"));
		} else {
			print $_;
		}
	}	
	elsif (m/warning:/ || m/^svn: E[\d]+:/) # warning
	{
		print(color($colors{"warning"}), $_, color("reset"));
	}
	elsif (m/^$svnName[^:]*: / || m/^svn server: /) # server message
	{
		print(color($colors{"server"}), $_, color("reset"));
	}
	else # Anything else
	{
		# Print normally.
		print(color("reset"), $_);
	}
	print "\n";
}

# Get the return code of the svn program and exit with that.
waitpid($svn_pid, 0);
exit ($? >> 8);
