#!/usr/bin/env perl
#
##########################################################################
#
# Name:		svni
# Version:	0.36
# Author:	Rene Uittenbogaard
# Date:		2012-08-14
# Usage:	svni	[ -a, --add ] [ -d, --debug ] [ -e, --editor <editor> ]
#			[ -f, --followup <command> ] [ --highlight [ syntax ] ]
#			[ -l, --lint <type>[,<type> ... ] ]
#			[ -m, --message <message> ] [ -N, --non-recursive ]
#			[ -u, --username <username> ]
#			[ --no-followup ] [ --no-lint ] [ <files> ... ]
#		svni	{ -r, --retry | -h, --help | -v, --version }
# Description:	svn interactive check-in
#		Allows the user to select individual files for
#		checking them in.
# Copyright:	This program is free software; you can redistribute it
#		and/or modify it under the terms of the GNU General Public
#		License version 3.
#		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.
#

############################################################################
# version info for *roff

our $ROFFVERSION = <<'=cut';

=pod

=for roff
.ds Yr 2012
.ds Vw @(#) svni 0.36
.de Vp
This manual pertains to \f(CWsvni\fP version \\$3.
..
.hy 0 \" hyphenation off

=cut

##########################################################################
# declarations

use Getopt::Long;
use File::Temp qw(:seekable);
use IO::File;

use constant {
	SVN_STATUSLINE_RE	=> qr/^(......\s+)(\S.*)$/,
	SVN_STATUSLINE_FORMAT	=> "%-8s%s\n",
	MTIME_FIELD		=> 9,
	FOLDMARKER_START	=> '{{{_svni_{{{',
	FOLDMARKER_END		=> '}}}_svni_}}}',
	RC_FILENAME		=> '.svnirc',
};

use constant DEFAULT_OPTIONS => {
	editor => $ENV{VISUAL} || $ENV{EDITOR} || 'vi',
	lint_  => {
		c => {
			regexp  => qr/\.c$/,
			command => "gcc -fsyntax-only",
		},
		javascript => {
			regexp  => qr/\.(?:js|json)$/,
			command => "rhino /usr/local/share/jslint.js",
		},
		perl => {
			regexp  => qr/\.(?:pl|pm)$/,
			command => "perl -cw",
		},
		php => {
			regexp  => qr/\.php$/,
			command => "php -l",
		},
		tex => {
			regexp  => qr/\.(?:tex|latex|sty|cls)$/,
			command => "if chktex -q -m all %s 2>&1 | grep .; " .
				   "then exit 1; fi",
		},
		xml => {
			regexp  => qr/\.xml$/,
			command => "xmllint --noout",
		},
	},
};

my $SECTION_1_2_SEPARATOR =
	"-- The lines below will make a selection what to commit   --";
my $SECTION_2_3_SEPARATOR_DIFF =
	"-- Difference overview --";
my $SECTION_2_3_SEPARATOR_NODIFF =
	"-- Differences: none --";
my $SECTION_2_EARLY_END =
	"Property changes on: ";

##########################################################################
# functions

sub by_mtime_desc {
	return +(stat $b)[MTIME_FIELD] <=> (stat $a)[MTIME_FIELD];
}

sub modeline {
	my ($options)   = @_;
	my $foldmarkers = FOLDMARKER_START . ',' . FOLDMARKER_END;
	my $highlight   = $options->{highlight};
	my $filetype    = $highlight
		? $highlight
		: defined($highlight) ? 'svni' : 'svn';
	return "-- vim: set filetype=$filetype foldmethod=marker " .
		"foldmarker=$foldmarkers: --\n";
}

sub print_usage
{
	my $smul      = qx(tput smul);
	my $rmul      = qx(tput rmul);
	my $type      = "${smul}type${rmul}";
	my $files     = "${smul}files${rmul}";
	my $editor    = "${smul}editor${rmul}";
	my $highlight = "${smul}syntax${rmul}";
	my $username  = "${smul}username${rmul}";
	my $command   = "${smul}command${rmul}";
	my $message   = "${smul}message${rmul}";
	print STDERR
		"Usage:\n",
		"svni [ -a, --add ] [ -d, --debug ] ",
			"[ -e, --editor $editor ]\n",
		"     [ -f, --followup $command ] ",
			"[ --highlight [ $highlight ] ]\n",
		"     [ -l, --lint ${type}[,$type ... ] ] ",
			"[ -m, --message $message ]\n",
		"     [ -N, --non-recursive ] ",
			"[ -u, --username $username ]\n",
		"     [ --no-followup ] [ --no-lint ] ",
			"[ $files ... ]\n",
		"svni { -r, --retry | -h, --help | -v, --version }\n",
		"\nSupported types for '-l': ",
		join(",", sort(get_known_types())), "\n";
	return;
}

sub print_version
{
	my $version = $ROFFVERSION;
	$version =~ s/.*Vw @\(#\) svni ([0-9.]+).*/$1/s;
	print "svni v$version\n";
	return;
}

sub get_known_types
{
	return keys %{${DEFAULT_OPTIONS()}{lint_}};
}

sub rcs_command
{
	# Allowed modes: status, diff, add, del, ci
	my ($mode, $options, $files) = @_;
	$files = join(' ', map { quotemeta } @$files);
	my $command = sprintf(
		"%s svn %s %s %s %s %s",
		($options->{debug} ? 'echo' : ''),
		$mode,
		$files,
		($options->{username}
			? ' --username=' . $options->{username} : ''),
		($options->{non_recursive} ? "-N" : ''),
		($options->{message_file}
			? "-F \Q$options->{message_file}\E" : ''),
	);
	return $command;
}

sub rcs_status_command
{
	my ($options, $files) = @_;
	my $nodebug_options = { %$options };
	$nodebug_options->{debug} = 0;
	return rcs_command('status', $nodebug_options, $files);
}

sub rcs_diff_command
{
	my ($options, $files) = @_;
	my $nodebug_options = { %$options };
	$nodebug_options->{debug} = 0;
	return rcs_command('diff', $nodebug_options, $files);
}

sub rcs_add_command
{
	my ($options, $files) = @_;
	return rcs_command('add', $options, $files);
}

sub rcs_del_command
{
	my ($options, $files) = @_;
	return rcs_command('del', $options, $files);
}

sub rcs_commit_command
{
	my ($options, $files) = @_;
	return rcs_command('ci', $options, $files);
}

sub parse_rcfile
{
	my $options = $_[0];
	my $rcfile = IO::File->new($ENV{HOME} . '/' . RC_FILENAME, "r");
	return unless defined $rcfile;
	while (defined($_ = $rcfile->getline())) {
		s/#.*//; # remove comments
		if (/^(auto_svn_add|debug|editor|followup|highlight|lint|
			message|username):\s*(.*?)\s*$/x)
		{
			$options->{$1} = $2;
			next;
		}
		if (/^lint_([^:]+):(.*)$/) {
			$options->{lint_}{$1}{command} = $2;
			next;
		}
	}
}

sub parse_cmdline
{
	my $options       = $_[0];
	my $auto_svn_add  = $options->{auto_svn_add};
	my $debug         = $options->{debug};
	my $editor        = $options->{editor};
	my $followup      = $options->{followup};
	my $lint          = $options->{lint};
	my $non_recursive = $options->{non_recursive};
	my $message       = '';
	my $highlight     = $options->{highlight};
	my $username      = $options->{username};
	my $help          = 0;
	my $retry         = 0;
	my $version       = 0;
	# Workaround: we can't combine negation and a string argument
	my $nolint        = 0;
	my $nofollowup    = 0;
	Getopt::Long::Configure(qw'bundling permute');
	unshift @ARGV, split(/\s+/, $ENV{SVNI_OPTIONS});
	unless (GetOptions(
			'a|add'           => \$auto_svn_add,
			'd|debug'         => \$debug,
			'e|editor=s'      => \$editor,
			'f|followup=s'    => \$followup,
			'h|help'          => \$help,
			'l|lint=s'        => \$lint,
			'N|non-recursive' => \$non_recursive,
			'm|message=s'     => \$message,
			'r|retry'         => \$retry,
			'highlight:s'     => \$highlight,
			'u|username=s'    => \$username,
			'v|version'       => \$version,
			'no-lint'         => \$nolint,
			'no-followup'     => \$nofollowup)
	) {
		print_usage();
		die "\n";
	}
	if ($help) {
		print_usage();
		exit 0;
	}
	if ($version) {
		print_version();
		exit 0;
	}
	$options->{auto_svn_add}  = $auto_svn_add;
	$options->{debug}         = $debug;
	$options->{editor}        = $editor;
	$options->{followup}      = $nofollowup    ? undef : $followup;
	$options->{lint}          = $nolint        ? undef : $lint;
	$options->{non_recursive} = $non_recursive;
	$options->{retry}         = $retry;
	$options->{highlight}     = $highlight;
	$options->{username}      = $username;
	if ($message) {
		$options->{message} .= "\n$message"; # concatenate
	}
	return;
}

sub parse_environment
{
	# parse environment variables except SVNI_OPTIONS
	# (those will be parsed by parse_cmdline)
	my $options = $_[0];
	my $editor  = $ENV{SVNI_EDITOR};
	if (defined $editor) {
		$options->{editor} = $editor;
	}
	my $checker;
	foreach (get_known_types()) {
		$checker = 'SVNI_LINT_' . uc($_);
		if (exists $ENV{$checker}) {
			$options->{lint_}{$_}{command} = $ENV{$checker};
		}
	}
}

sub are_you_sure
{
	my @lines = @_;
	$lines[-1] .= ", are you sure? (y/n) ";
	print join "\n", @lines;
	my $ans = <STDIN>;
	return $ans =~ /^\s*y/i;
}

sub syntax_error_count
{
	my ($options, @files) = @_;
	my $errors = 0;
	my %syntax_checkers = %{$options->{lint_}};
	my (%selected_checkers, $command);
	@selected_checkers{split(/,/, $options->{lint})} = ();
	foreach my $type (
		grep { exists($selected_checkers{$_}) } keys %syntax_checkers
	) {
		print "Checking $type syntax\n";
		$command = $syntax_checkers{$type}{command};
		unless ($command =~ /%s/) {
			$command .= ' %s';
		}
		foreach (grep { $_ =~ $syntax_checkers{$type}{regexp} } @files)
		{
			# skip nonexistent files.
			if (-e $_) {
				print " $_...\n";
				system sprintf($command, quotemeta $_)
					and $errors++;
			}
		}
	}
	return $errors;
}

sub get_svnstatusline_filename
{
	my ($line) = @_;
	$line =~ SVN_STATUSLINE_RE;
	return $2;
}

sub get_retryfile
{
	my @retryfiles =
		sort by_mtime_desc grep { -o } glob '/tmp/svni.status.*';
	die "No svni retry files found\n" unless @retryfiles;
	my $retryfile  = IO::File->new($retryfiles[0], "r");
	return $retryfile->getlines();
}

sub write_status_section
{
	my ($svnfile, $options, $files) = @_;
	my (%files_statted, $statusline);
	my $errors = 0;
	if ($options->{message}) {
		$svnfile->print($options->{message} . "\n");
	}
	$svnfile->print("\n",
		"-- Lines starting with '--' will be ignored               --\n",
		"$SECTION_1_2_SEPARATOR\n",
		"-- Files with '+' in the first column will be svn-added   --\n",
		"-- Files with '-' in the first column will be svn-removed --\n",
		"\n");
	open my $STATUS, '-|', rcs_status_command($options, $files) . " 2>&1"
		or die "Cannot start svn status command: $!";
	while ($statusline = <$STATUS>) {
		if ($options->{auto_svn_add}) {
			$svnfile->print(map { s/^\?/+/; $_ } $statusline);
		} else {
			$svnfile->print($statusline);
		}
		# bookkeep files returned by 'svn status'
		$files_statted{get_svnstatusline_filename($statusline)} = 1;
	}
	close $STATUS;
	# If files on the commandline are not reported by 'svn status',
	# add them to the outputfile anyway.
	foreach (grep { !$files_statted{$_} } @$files) {
		$svnfile->printf(
			SVN_STATUSLINE_FORMAT,
			($options->{auto_svn_add} ? '+' : '.'),
			$_);
	}
	if ($options->{lint}) {
		$errors = syntax_error_count($options, keys %files_statted);
		print "\n";
	}
	return $errors;
}

sub write_diff_section
{
	my ($svnfile, $options, $files) = @_;
	my ($difference, $diff_started);
	open my $DIFF, '-|', rcs_diff_command($options, $files) . " 2>&1"
		or die "Cannot start svn diff command: $!";
	while ($difference = <$DIFF>) {
		chomp $difference;
		if ($difference =~ /^Index: /) {
			if (!$diff_started) {
				$diff_started = 1;
				$svnfile->print(
					"\n$SECTION_2_3_SEPARATOR_DIFF\n\n");
			} else {
				$svnfile->print(FOLDMARKER_END."\n");
			}
			$svnfile->print("$difference ".FOLDMARKER_START."\n");
		} else {
			$svnfile->print("$difference\n");
		}
	}
	if ($diff_started) {
		$svnfile->print(FOLDMARKER_END."\n");
	} else {
		$svnfile->print("$SECTION_2_3_SEPARATOR_NODIFF\n");
	}
	close $DIFF;
	return;
}

sub setup_svnfile
{
	my ($options, @files) = @_;
	my (@retrylines, $error_count);
	if ($options->{retry}) {
		@retrylines = get_retryfile();
	}
	my $svnfile = File::Temp->new(
		TEMPLATE => 'svni.status.XXXXXXXXXXXX',
		DIR      => '/tmp',
		UNLINK   => 0,
	);
	die "Cannot open a tempfile for writing: $!" unless $svnfile;
	if ($options->{retry}) {
		$svnfile->print(@retrylines);
		# Keep the file open (it will be closed later).
		return $svnfile;
	}
	$error_count = write_status_section(
		$svnfile, $options, \@files);
	if ($error_count
		&& !are_you_sure("\nSyntax errors found in input files")
	) {
		die "Aborted\n";
	}
	write_diff_section($svnfile, $options, \@files);
	$svnfile->printf("\n" . modeline($options));
	# Keep the file open (it will be closed later).
	return $svnfile;
}

sub edit_svnfile
{
	my ($options, $svnfile) = @_;
	my $editor = $options->{editor};
	system "$editor \Q$svnfile\E"
		and die "Failed to start editor '$editor': $!";
	return;
}

sub write_msgfile
{
	my $commit_msg = shift;
	my $msgfile = File::Temp->new(
		TEMPLATE => 'svni.message.XXXXXXXXXXXX',
		DIR      => '/tmp',
		UNLINK   => 0,
	);
	die "Cannot open a message file for writing: $!" unless $msgfile;
	$msgfile->print($commit_msg);
	$msgfile->close()
		or die "Error while closing message file '$msgfile': $!";
	return $msgfile;
}

sub process_svnfile
{
	my ($options, $svnfile) = @_;
	my @commit_msg = ();
	my @files      = ();
	my @add_files  = ();
	my @del_files  = ();
	# section 1 is the commit message
	# section 2 is the file selection
	# section 3 is the diff overview
	my $in_section = 1;

	# If the message file has been replaced by another file (as vi(1) on
	# MacOS seem to do) instead of rewritten, reopen it.
	unless ($svnfile->cmpstat("$svnfile")) {
		my $tempfilename = "$svnfile";
		$svnfile->close();
		$svnfile = IO::File->new($tempfilename, "r");
		if (!defined $svnfile) {
			die "Error: '$svnfile' was replaced, but cannot " .
				"be reopened for reading: $!";
		}
	}
	$svnfile->seek(0, SEEK_SET)
		or die "Cannot open '$svnfile' for reading: $!";
	while (defined($_ = $svnfile->getline())) {
		if ($options->{debug}) {
			print "$in_section: $_";
		}
		chomp;
		if ($in_section == 1) {
			if (/^$SECTION_1_2_SEPARATOR/) {
				# transition 1->2
				$in_section = 2;
				next;
			} elsif (!/^--/) {
				# compose message, but discard comments
				push @commit_msg, $_;
				next;
			}
		} elsif ($in_section == 2) {
			if (	$_ eq $SECTION_2_3_SEPARATOR_DIFF or
				$_ eq $SECTION_2_3_SEPARATOR_NODIFF or
				$_ =~ /^$SECTION_2_EARLY_END/
			) {
				# transition 2->3
				$in_section = 3;
				last;
			} elsif (!/^--/) {
				# compose file lists, but discard comments
				($status, $filename) = $_ =~ SVN_STATUSLINE_RE;
				if (defined($status) and $status !~ /^\?/) {
					push @files,     $filename;
				}
				if ($status =~ /^\+/) {
					push @add_files, $filename;
				}
				if ($status =~ /^-/) {
					push @del_files, $filename;
				}
				next;
			}
		} else { # $in_section == 3
			last;
		}
	}
	$svnfile->close();

	my $commit_msg = join "\n", @commit_msg, '';
	my $msgfile    = write_msgfile($commit_msg);

	unless (@files) {
		print "No files to check in\n";
		# success: discard files
		unlink $msgfile, $svnfile;
		return 0;
	}
	if ($commit_msg !~ /\S/) {
		print "No commit message, aborting\n";
		return 0;
	}
	if (@add_files and system rcs_add_command($options, \@add_files)) {
		die join "\n",
			"Failed to 'svn add' the following files:",
			@add_files, ' ';
	}
	if (@del_files and system rcs_del_command($options, \@del_files)) {
		die join "\n",
			"Failed to 'svn delete' the following files:",
			@del_files, ' ';
	}

	$options->{message_file} = $msgfile;
	system rcs_commit_command($options, \@files)
		and die join "\n",
			"Failed to check in the following files:",
			@files, ' ';
	# success: discard files
	unless ($options->{debug}) {
		unlink $msgfile;
	}
	return 1;
}

sub run_followup_command
{
	my ($options, $svnfile) = @_;
	my $command = $options->{followup};
	return unless defined $command;
	unless ($command =~ /%s/) {
		$command .= ' %s';
	}
	if (system sprintf($command, quotemeta $svnfile)) {
		die "Error: followup command exited with an error\n";
	}
	return;
}

sub main
{
	my $options = DEFAULT_OPTIONS;
	my ($svnfile, $work_done);
	# next lines pass $options by reference
	parse_rcfile($options);      # read RC file first
	parse_environment($options); # environment takes precedence
	parse_cmdline($options);     # commandline takes precedence
	$svnfile = setup_svnfile($options, @ARGV);
	edit_svnfile($options, $svnfile);
	$work_done = process_svnfile($options, $svnfile);
	if ($work_done) {
		if (!$options->{debug}) {
			run_followup_command($options, $svnfile);
			unlink $svnfile unless $options->{debug};
		} elsif (defined $options->{followup}) {
			print "Not running followup command: ",
				$options->{followup}, "\n";
		}
	}
	return;
}

##########################################################################
# main

main();

__END__

##########################################################################
# manual

=pod

=for section 1

=begin html

<style type="text/css">
  body {
    font-family: Arial, Helvetica, sans-serif;
  }
  pre {
    border-left: 2px solid #ccc;
  }
  dt, code {
    font-family: "Courier New", Courier, fixed;
  }
  h1 {
    font-size: 14pt;
  }
  img {
    border: 0px;
  }
  .sflogo {
    position: absolute;
    top: 15px;
    right: 15px;
  }
</style>

<a href="http://sourceforge.net/projects/svni"><img
src="http://sourceforge.net/sflogo.php?group_id=397936&type=1"
class="sflogo" width="88" height="31" alt="SourceForge Logo"></a>

=end html

=head1 NAME

svni - Interactive subversion check-in

=head1 SYNOPSIS

C<svni [ -a | --add ] [ -d | --debug ] [ -e | --editor >I<editor>C< ]>

C<     [ -f | --followup >I<command>C< ] [ --highlight [ >I<syntax>C< ] ] >

C<     [ -l | --lint >I<type>C<[,>I<type ...>C< ] ] >
C<[ -m | --message >I<message>C< ]>

C<     [ -N | --non-recursive ] [ -u | --username >I<username>C< ] >

C<     [ --no-followup ] [ --no-lint ] [ >I<files ...>C< ]>

C<svni { -r | --retry | -h | --help | -v | --version } >

=head1 DESCRIPTION

C<svni> can be used to interactively select files to check in into a
subversion repository.

C<svni> will first run C<svn status> and C<svn diff> on the specified
files (C<.> by default). Then a temporary file will be constructed
with the following format:

    -- Lines starting with '--' will be ignored               --
    -- The lines below will make a selection what to commit   --
    -- Files with '+' in the first column will be svn-added   --
    -- Files with '-' in the first column will be svn-removed --
    
    ?       theme/foreground.css
    M       theme/icons.css
    M       theme/style.css
    A       theme/icons/user_add.png
    
    -- Difference overview --
    
    +-- 17 lines: Index: theme/icons.css---------------------------
    +-- 13 lines: Index: theme/style.css---------------------------
    
    -- vim: set filetype=svni foldmethod=marker: --

This file will be presented to the user in an editor of the user's choice.
The user can add a commit message at the start of the file and use the list
of files in the middle section to make a selection of files to check in.
The character in the first column is the C<svn status> character, or period
B<.> if the file is up-to-date.  Files that are marked with a question mark
B<?> or period B<.> in the first column will not be checked in, unless the
user changes it to a plus sign B<+>, in which case C<svni> will add them to
the repository first (using C<svn add>).  If the first character is changed
to a minus sign B<->, the file will be deleted from the repository (using
C<svn del>).

C<svni> will automatically mark unversioned files for adding if the B<-a>
commandline option is provided.

The bottom section of the file contains C<svn diff> information enclosed in
svni-specific vim(1) fold markers. If vim(1) is used as the editor, these
folds will normally be closed at editor startup time. When opened, they will
show which differences are about to be committed, as follows:

    -- Difference overview --
    
    Index: theme/icons.css {{{_svni_{{{
    ===============================================================
    --- theme/icons.css     (revision 7233)
    +++ theme/icons.css     (working copy)
    @@ -7,6 +7,11 @@
       background-repeat: no-repeat;
     }
    
    +.icon-status-inactive {
    +  background-image: url(icons/status_inactive.png) !important;
    +  background-repeat: no-repeat;
    +}
    +
     .icon-status-concept {
       background-image: url(icons/fam/pencil.png) !important;
       background-repeat: no-repeat;
    }}}_svni_}}}
    +-- 13 lines: Index: theme/style.css---------------------------
    
    -- vim: set filetype=svni foldmethod=marker: --

In this section, all lines will be ignored, so lines starting with a plus
sign B<+> or minus sign B<-> have no special meaning and will not cause
any files to be added or deleted.

For more information about folds, use the vim(1) command C<:help folds>.

If the user wishes to abort the current action, it suffices to empty the
list of filenames.

If no commit message is entered, the commit will be aborted, but the
temporary file will be kept. It can be edited again using C<svni -r>
(see below).

=head1 OPTIONS

=over 4

=item -a, --add

Unversioned files will be automatically marked for addition with C<svn add>.
Unversioned files are normally shown with a question mark B<?> in the first
column; using this option will automatically convert these to pluses B<+>
before opening the editor.

=item -d, --debug

Does not perform an actual C<svn add> or C<svn ci>, but echoes the
commands to standard output instead. Also, does not remove the temporary
message file.

=item -e, --editor

Used to indicate which editor should be spawned.

=item -f, --followup I<command>

Specifies a command to run after a succesful commit.

=item -h, --help

Print usage information and exit.

=item --highlight [ I<syntax> ]

Specifies which syntax highlighting to select in the vim(1) modeline.
The default, in absence of this option, is B<svn> syntax. If the vim syntax
file for C<svni> has been installed, then the presence of this option will
select B<svni> syntax, which has the added benefit of recognizing and
highlighting lines that start with B<+> or B<->, and using I<diff> syntax
for regions between foldmarkers.

A syntax may be selected explicitly by specifying it as an argument to the
C<--highlight> option. The defaults are B<svn> if the option is absent,
B<svni> if present without an argument.

=item -l, --lint I<type>[,I<type> ... ]

The input files of the specified file types are checked for syntax errors.
If any errors are found, a warning is emitted and the user is asked for
confirmation before continuing.

Currently, the default syntax checkers are:

=begin html

<table border="0">
<tr>
	<td><a name="c" class="item">C</a></td>
	<td>:</td>
	<td>gcc -fsyntax-only</td>
</tr>
<tr>
	<td><a name="javascript" class="item">JavaScript</a></td>
	<td>:</td>
	<td>rhino /usr/local/share/jslint.js</td>
</tr>
<tr>
	<td><a name="perl" class="item">Perl</a></td>
	<td>:</td>
	<td>perl -cw</td>
</tr>
<tr>
	<td><a name="php" class="item">PHP</a></td>
	<td>:</td>
	<td>php -l</td>
</tr>
<tr>
	<td><a name="tex" class="item">TeX</a></td>
	<td>:</td>
	<td>if chktex -q -m all %s 2>&1 | grep .; then exit 1; fi</td>
</tr>
<tr>
	<td><a name="xml" class="item">XML</a></td>
	<td>:</td>
	<td>xmllint --noout</td>
</tr>
</table>

=end html

=begin roff

.in +4n
.TS
lw(14n) | lw(30n).
_
C:	gcc -fsyntax-only
JavaScript:	rhino /usr/local/share/jslint.js
Perl:	perl -cw
PHP:	php -l
TeX:	T{
if chktex -q -m all %s 2>&1 | grep .; then exit 1; fi
T}
XML:	xmllint --noout
_
.TE
.in

=end roff

These default syntax checking commands can be overruled using the
environment variables SVNI_LINT_* or by using a config file (see below).

=item -m, --message I<message>

Fill the commit message with an initial value. Note that the changes
will B<not> be automatically committed.

=item --no-followup

Don't run any followup command specified in the config file or on the
commandline.

=item --no-lint

Don't run any syntax checkers specified in the config file or on the
commandline.

=item -N, --non-recursive

Use the B<-N> flag in C<svn> commands. This ensures that add and delete
actions are not performed recursively.

In case the user wants to add a new directory, but only some of the
files contained in it, this option must be used.

In most other cases where the user wants to perform an C<svn add> or
C<svn del> action, this option must not be used.

=item -r, --retry

If a previous commit has failed, the commit message file has not been
deleted.  This option will instruct C<svni> to pick up the most recent
message file, and open it in the editor to try the commit again.

=item -u, --username I<username>

Set the username as indicated.

=item -v, --version

Print the current program version and exit.

=back

=head1 ENVIRONMENT

=over 4

=item SVNI_EDITOR

Used to specify the editor. Takes precedence over the usual VISUAL and
EDITOR commands, and over the setting in the F<.svnirc> file.

=item SVNI_LINT_C

=item SVNI_LINT_JAVASCRIPT

=item SVNI_LINT_PERL

=item SVNI_LINT_PHP

=item SVNI_LINT_TEX

=item SVNI_LINT_XML

Specify alternate commands to check the syntax of source files, I<e.g.>:

    SVNI_LINT_JAVASCRIPT='jslint_wrapper -s'
    SVNI_LINT_TEX=chktex_wrapper

C<svni> assumes that the name of the file to be checked can be appended
to this command, unless the string B<%s> occurs in the commandline,
in which case C<svni> will replace it with the name of the file to be
checked. The default TeX syntax checking command shows an example
of such usage.

=item SVNI_OPTIONS

Used to specify default commandline options, I<e.g.>:

    SVNI_OPTIONS='-l php,javascript --highlight'

=back

=head1 CONFIG FILE

C<svni> also reads config options from a user-specific F<$HOME/.svnirc>
file.  Lines in this file should have the form C<key:value>. Everything
after a B<#> character on the same line is discarded as a comment.
An example file with the supported options:

    auto_svn_add:0 # can be 0 or 1
    #debug:0 # can be 0 or 1
    editor:vim -X
    #followup:svn_after_commit.sh
    highlight:svni
    lint:php,javascript,perl,tex
    #lint_c:
    lint_javascript:jslint_wrapper -s
    #lint_perl:
    #lint_php:
    lint_tex:chktex_wrapper
    #lint_xml:
    message:Standard commit message header
    #username:ruittenb

=head1 SYNTAX HIGHLIGHTING

C<svni> is bundled with a vim(1) syntax file F<svni.vim>. When
installed, it can apply B<svn> syntax highlighting (including the
C<svni>-specific command characters B<+> and B<->) to the top part
of the temporary message file, and B<diff> syntax highlighting to
the bottom part of the file.

=head1 BUGS and WARNINGS

None known.

=head1 VERSION

=for roff
.PP \" display the 'pertains to'-macro
.Vp \*(Vw

=head1 AUTHOR and COPYRIGHT

=for roff
.\" the \(co macro only exists in groff
.ie \n(.g Copyright \(co \*(Yr, Ren\('e
.el       Copyright (c) \*(Yr, Rene\*'
Uittenbogaard (ruittenb@users.sourceforge.net).
.PP

=for html
Copyright &copy; Ren&eacute; Uittenbogaard
(ruittenb@users.sourceforge.net).

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3.

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.

C<svni> can be obtained from its project page at Sourceforge:
L<http://sourceforge.net/projects/svni>

=head1 SEE ALSO

chktex(1), rhino(1), svn(1), vim(1), xmllint(1).

=cut

