#!/usr/bin/perl

=head1 NAME

svn_filesystem_doctor - analyze and manipulate svn repositories on a filesystem

=head1 SYNOPSIS

svn_filesystem_doctor S<[ B<--printf>="I<string>" ]>
                S<[ B<--oldsvnadminpath>="I<command_path>" ]>
                S<[ B<--svnadminpath>="I<command_path>" ]>
		S<[ B<--autochange> ]> S<[ B<--backup> ]>
	        S<[ B<--stdin> ]> S<[ B<--findall>="I<starting_path>" ]>
                S<[ B<--apacheconf> ]>

At minimum, either B<stdin> or the B<findall> flag must be specified in order
to launch B<svn_filesystem_doctor>.

The B<printf> I<string> can consist of the following escaped characters:

=over 4

=item %d

Directory path to a subversion repository.

=item %s

The format schema of the subversion repository.  A value of "0N" means
that the format schema equals version 0 that it does not work with the current
B<svnadmin> command.  A value of "0Y" means version 0, and that it does work
with the current B<svnadmin> command.  A value of "13Y" means version 13
and that it does work with the current B<svnadmin> command.  And so on.

=back

=head1 DESCRIPTION

Looks at an existing filesystem and diagnoses the locations and states
of various subversion repositories (and this means "repositories", not
working copies).  This program also facilitates the generation
of plain-text backups as well as upgrading the format db schema
based on new releases of subversion.

=head2 Getting the subversion repositories

The B<findall> and B<stdin> flags control the repositories that this
script works with.  The B<findall> flag will probe the entire filesystem
and look for likely locations of subversion repositories (using heuristics
related to directory structure).  The B<stdin> flag will alternatively
accept a list of subversion directory filesystem paths (for which to apply
the B<--printf>, B<--autochange>, or other functionalities).

=head2 Locating the svnadmin executable

The B<svnadminpath> and B<oldsvnadminpath> flags specify the location
of the B<svnadmin> executables necessary to implement various
svn_filesystem_doctor functionalities.  Ordinarily, just the B<svnadminpath>
variable would need to be specified (if it is not specified, the regular shell
command path is used).  The B<oldsvnadminpath> variable is specified for
situations where the older version of B<svnadmin> is needed to perform db
format schema upgrades (C<oldsvnadmin dump> followed by a C<currentsvnadmin
load>)....

=head2 Functionalities of svn_filesystem_doctor

=over 4

=item * Generate an apache configuration file

From the determined list of subversion repositories, an apache configuration
text string is generated.  This functionality is invoked by the B<apacheconf>
flag.  The configuration text is passed to standard output.

=item * Autochange: update db format schemas used by subversion repositories

Subversion repositories are updated from an older database format to a
newer database format.  This functionality is invoked by the B<autochange>
flag.  The manual procedure for this is:

=over 4

C<svnadmin1 dump reposname E<gt> reposname.dump>

C<svnadmin2 load reposname E<lt> reposname.dump>

=back

where C<svnadmin1> and C<svnadmin2> are the old and new binary B<svnadmin>
executables respectively.  C<svnadmin1> is specified by the
B<oldsvnadminpath> variable.  C<svnadmin2> is specified by the
B<svnadminpath> variable (or, if not specified, by the default location
in the shell command path).

=item * Backup by generating plain-text files for each subversion repository

This dumps (creates) plain-text repository backup files for every specified
subversion repository.  This functionality is invoked by the B<backup> flag.
The list of generated backup files is sent to standard output.

=item * Print out repository information (printf)

This functionality is invoked by the B<printf> command line argument
(and this is also the default functionality if no functionality is
specified).  The repository locations (and potentially other information)
is output to standard output.

=back

=cut

use Getopt::Long; # Get specified options from the command line.

# ============================= Process command-line arguments and error-check.
my $usage=(<<END);
Usage of svn_filesystem_doctor.

SYNOPSIS
    svn_filesystem_doctor [ --printf="*string*" ]
    [ --oldsvnadminpath="*command_path*" ]
    [ --svnadminpath="*command_path*" ] [ --autochange ] [ --backup ]
    [ --stdin ] [ --findall="*starting_path*" ] [ --apacheconf ]

    At minimum, either stdin or the findall flag must be specified in order
    to launch svn_filesystem_doctor.

    The printf *string* can consist of the following escaped characters:

    %d  Directory path to a subversion repository.

    %s  The format schema of the subversion repository. A value of "0N"
        means that the format schema equals version 0 that it does not work
        with the current svnadmin command. A value of "0Y" means version 0,
        and that it does work with the current svnadmin command. A value of
        "13Y" means version 13 and that it does work with the current
        svnadmin command. And so on.

DESCRIPTION
    Looks at an existing filesystem and diagnoses the locations and states
    of various subversion repositories (and this means "repositories", not
    working copies). This program also facilitates the generation of
    plain-text backups as well as upgrading the format db schema based on
    new releases of subversion.

  Getting the subversion repositories

    The findall and stdin flags control the repositories that this script
    works with. The findall flag will probe the entire filesystem and look
    for likely locations of subversion repositories (using heuristics
    related to directory structure and also applying the "svnadmin youngest"
    command. The stdin flag will alternatively accept a list of subversion
    directory filesystem paths (for which to apply the --printf,
    --autochange, or other functionalities).

  Locating the svnadmin executable

    The svnadminpath and oldsvnadminpath flags specify the location of the
    svnadmin executables necessary to implement various svn_filesystem_doctor
    functionalities. Ordinarily, just the svnadminpath variable would need
    to be specified (if it is not specified, the regular shell command path
    is used). The oldsvnadminpath variable is specified for situations where
    the older version of svnadmin is needed to perform db format schema
    upgrades ("oldsvnadmin dump" followed by a "currentsvnadmin load")....

  Functionalities of svn_filesystem_doctor

    * Generate an apache configuration file
        From the determined list of subversion repositories, an apache
        configuration text string is generated. This functionality is
        invoked by the apacheconf flag. The configuration text is passed to
        standard output.

    * Autochange: update db format schemas used by subversion repositories
        Subversion repositories are updated from an older database format to
        a newer database format. This functionality is invoked by the
        autochange flag. The manual procedure for this is:

            "svnadmin1 dump reposname > reposname.dump"

            "svnadmin2 load reposname < reposname.dump"

        where "svnadmin1" and "svnadmin2" are the old and new binary
        svnadmin executables respectively. "svnadmin1" is specified by the
        oldsvnadminpath variable. "svnadmin2" is specified by the
        svnadminpath variable (or, if not specified, by the default location
        in the shell command path).

    * Backup by generating plain-text files for each subversion repository
        This dumps (creates) plain-text repository backup files for every
        specified subversion repository. This functionality is invoked by
        the backup flag. The list of generated backup files is sent to
        standard output.

    * Print out repository information (printf)
        This functionality is invoked by the printf command line argument (and
	this is also the default functionality if no functionality is
        specified).  The repository locations (and potentially other
        information) is output to standard output.

END
my %h; # This has will store the command line options values.
my $retval = &GetOptions(\%h,
			 "printf=s","oldsvnadminpath=s","svnadminpath=s",
			 "autochange","backup","findall=s","stdin",
			 "apacheconf");

# Command option syntax must be correct.
unless ($retval)
  {
    print($usage);
    exit(1);
  }

# At minimum, either the findall or the stdin flag needs to be specified
# (so that a list of repositories can be obtained to work on).
unless ($h{'findall'} or $h{'stdin'})
  {
    print($usage);
    exit(1);
  }

# Both findall and stdin flags cannot be specified together.
if ($h{'findall'} and $h{'stdin'})
  {
    print('findall and stdin cannot both be specified'."\n");
    print($usage);
    exit(1);
  }

# If specified, the findall path should exist on the filesystem.
if (defined($h{'findall'}))
  {
    unless (-e $h{'findall'})
      {
	print('The findall variable does not exist on the'.
	      ' filesystem'."\n");
	print($usage);
	exit(1);
      }
  }

# If specified, the oldsvnadminpath should exist on the filesystem.
if (defined($h{'oldsvnadminpath'}))
  {
    unless (-x $h{'oldsvnadminpath'})
      {
	print('The oldsvnadminpath variable does not exist as an executable '.
	      'on the filesystem'."\n");
	print($usage);
	exit(1);
      }
  }

# If specified, the findall path should exist on the filesystem.
if (defined($h{'svnadminpath'}))
  {
    unless (-x $h{'svnadminpath'})
      {
	print('The svnadminpath variable does not exist as an executable on '.
	      ' the filesystem'."\n");
	print($usage);
	exit(1);
      }
  }

# If autochange is specified, then oldsvnadminpath should be specified.
if ($h{'autochange'})
  {
    unless ($h{'oldsvnadminpath'})
      {
	print('For autochange, the oldsvnadminpath value must be specified.'.
	      "\n");
	print($usage);
	exit(1);
      }
  }

# ==================== Gather the list of subversion repositories to work with.
my @subversion_repository_list;
if ($h{'stdin'}) # Subversion repository directory list coming through stdin?
  {
    my @a = <>;
    @subversion_repository_list = map {chomp; $_} @a;
  }
elsif ($h{'findall'}) # Scan, starting from specified location in filesystem.
  {
 my @a = `find $h{'findall'} -maxdepth 1000 -type d -name "hooks" 2>/dev/null`;
    foreach my $possible (@a)
      {
	chomp($possible);
	$possible =~ s/\/[^\/]*$//;
	opendir(SDIR,$possible);
	my @otherdirs = grep {!/^\.\.?$/} readdir(SDIR);
	closedir(SDIR);
	my $score = 0; # The "heuristical measure" as to whether = svn repos. 
	foreach my $odir (@otherdirs)
	  {
	    if ($odir eq 'locks')
	      {
		$score++;
	      }
	    if ($odir eq 'db')
	      {
		$score++;
	      }
	  }
	if ($score > 1) # Score is good?
	  {
	    push(@subversion_repository_list,$possible); # Add to list.
	  }
      }
  }

# ============================================ Locate the svnadmin executables.
my $oldsvnadmin; # Set to the svnadmin command (of old database format schema).
my $currentsvnadmin;# Set to the svnadmin command (of desired format schema).

# Set the variables.
if ($h{'oldsvnadminpath'})
  {
    $oldsvnadmin = $h{'oldsvnadminpath'};
  }
if ($h{'svnadminpath'})
  {
    $currentsvnadmin = $h{'svnadminpath'};
  }
else
  {
    $currentsvnadmin = 'svnadmin';
    system("$currentsvnadmin --version 2>/dev/null");
    if ($? == 127)
      {
	print('Cannot find the current svnadmin command.'."\n");
	exit(1);
      }
  }

# ================================== Determine the function(s) to be performed.
my @functions; # List of functions to perform on the repository list.
if ($h{'printf'})
  {
    push(@functions,'printf');
  }
if ($h{'autochange'})
  {
    push(@functions,'autochange');
  }
if ($h{'backup'})
  {
    push(@functions,'backup');
  }
if ($h{'apacheconf'})
  {
    push(@functions,'apacheconf');
  }

if (!@functions) # printf is the default
  {
    push(@functions,'printf');
  }

# ========================================================= Carry out the task.
foreach my $function (@functions)
  {
    if ($function eq 'printf') # Output information.
      {
	foreach my $repos (@subversion_repository_list)
	  {
	    my $p = $h{'printf'};
	    unless ($p)
	      {
		$p = $repos."\n";
	      }
	    else
	      {
		$p =~ s/^\"//;
		$p =~ s/\"$//;
		$p =~ s/\\t/\t/g;
		$p =~ s/\\n/\n/g;
		$p =~ s/\%d/$repos/g;
	      }
	    if ($p =~ /\%s/)
	      {
		my $format;
		if (-e "$repos/format")
		  {
		    $format = `cat $repos/format`;
		    chomp($format);
		  }
		else
		  {
		    $format = 0;
		  }
		$p =~ s/\%s/$format/g;
	      }
	    print($p);
	  }
      }
    if ($function eq 'backup') # Create plaintext backups (svnadmin dump).
      {
	my $timestamp = `date +"%Y%m%d%H%M%S"`;
	chomp($timestamp);
	foreach my $repos (@subversion_repository_list)
	  {
	    system("$currentsvnadmin dump $repos > $repos.$timestamp.dump");
	    if ($?)
	      {
		print('FAILURE:'.$repos."\n");
	      }
	    else
	      {
		print('SUCCESS:'.$repos."\n");
		print('(RESTORE):'.
		      "mv $repos $repos.old; $currentsvnadmin create $repos; ".
		      "$currentsvnadmin load $repos < $repos.$timestamp.dump".
		      "\n");
	      }
	  }
      }
    if ($function eq 'autochange') # Try to convert database format schemas.
      {
	my $timestamp = `date +"%Y%m%d%H%M%S"`;
	chomp($timestamp);
	foreach my $repos (@subversion_repository_list)
	  {
	    system("$oldsvnadmin dump $repos > $repos.$timestamp.dump");
	    system("mv $repos > $repos.$timestamp.old") unless $?;
	    system("$currentsvnadmin create $repos") unless $?;
	    system("$currentsvnadmin load $repos < $repos.$timestamp.dump")
		unless $?;
	    system("rm -f $repos.$timestamp.old") unless $?;
	    if ($?)
	      {
		print('FAILURE:'.$repos."\n");
	      }
	    else
	      {
		print('SUCCESS:'.$repos."\n");
	      }
	  }
      }
    if ($function eq 'apacheconf') # Generate an apache configuration.
      {
	my $timestamp = `date +"%Y%m%d%H%M%S"`;
	foreach my $repos (@subversion_repository_list)
	  {
	    my $reposdir = $repos;
	    $reposdir =~ s/^.*\/([^\/]*)$/$1/;
	    print(<<END);
<Directory $repos>
    Options FollowSymLinks Indexes
    AllowOverride All
</Directory>
<Location /svn/$reposdir>
   DAV svn
   SVNPath $repos

   # Limit write permission to list of valid users.
   <LimitExcept GET PROPFIND OPTIONS REPORT>
      # Require SSL connection for password protection.
      # SSLRequireSSL

      AuthType Basic
      AuthName "Authorization Realm"
      AuthUserFile /subversion_users/passwdfile
      Require valid-user
   </LimitExcept>
</Location>
END
          }
      }
  }


