svn_update.pl   [plain text]


#!/usr/bin/perl -w

# ====================================================================
#
# svn_update.pl
#
# Put this in your path somewhere and make sure it has exec perms.
# Do your thing w/subversion but when you would use 'svn update'
# call this script instead.
#
# ====================================================================
# Copyright (c) 2000-2004 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-1.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/.
# ====================================================================


# WHY THE NEED FOR THIS SCRIPT?
#
# Currently, the subversion server will attempt to stream all file
# data to the client at once for _each_ merge candidate.  For cases
# that have >1 file and/or the complexity of the merge for any
# given file(s) that would require >n minutes, where n is the
# server's magic timeout (5 min.??), the server will timeout.  This
# leaves the client/user in an unswell state.  See issue #2048 for
# details http://subversion.tigris.org/issues/show_bug.cgi?id=2048.
#
# One solution is to wrap the 'svn update' command in a script that
# will perform the update one file at a time.  The problem with
# this solution is that it defeats the beneficial atomic nature of
# subversion for this type of action.  If commits are still coming
# in to the repository, the value of "HEAD" might change between each
# of these update operations.
#
# Another solution, the one that this script utilizes, passes the
# --diff3-cmd directive to 'svn update' using a command which forces
# a failed contextual merge ('/bin/false', for example).  These faux
# merge failures cause subversion to leave all of the accounting files
# involved in a merge behind and puts them into the 'conflict'
# state.  Since all the data required for all the merges took place
# at that exact moment atomicity is preserved and life is swell.

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

# This is required for copy() command.  I believe it's a standard
# module.  If there's a doubt run this from a shell:
#	perl -e 'use File::Copy;'
# If you don't get any complaint from perl you're good.  Otherwise
# comment this line out and change the $backup_mine_files to 0.
use File::Copy;

# This forces backing up of the .mine files for reference even
# after the resolved command.  The backups will be stored as
# <filename.username>
$backup_mine_files=1;

# Choose your favorite graphical diff app.  If it's not here, just add
# the full path to it and the style of the options to the
# %DIFF3CMD_hash below.
$DIFF3CMD="xcleardiff";

# Override the diff3-cmd in the config file here.
# If this is an empty string it'll use the config file's
# setting.
#$d3cmdoverride="";
$d3cmdoverride="/bin/false";

# Add more diff programs here.
# For the internal, discovered, file parameters:
#	+A+ ==> mine
#	+B+ ==> older
#	+C+ ==> latest
#	+D+ ==> Destination (The output of your merged code would go here.
#                            This would, generally, be whatever
#                            $(basename <+A+> .mine) would evaluate to.
# But you can feel free to do something like these:
# 	"+B+ +C+ +A+ -out +D+.bob"
# 	"+B+ +A+ +C+ -out /tmp/bob"
# Just note that the '+' (plus) are to limit the false positives in the
# search and replace sub.
#
# HAVING THE CORRECT PATH, AND ARGS FOR YOUR DIFF PROGRAM, IS CRITICAL!!
%DIFF3CMD_hash=(# Ahh...hallowed be thy name.
                "/opt/atria/bin/xcleardiff" => "+A+ +B+ +C+ -out +D+",
                # This one is slow.
                "/usr/bin/kdiff3"           => "+A+ +B+ +C+ -o +D+",
                # This one's slow and it sucks!(BUGGY)
                "/usr/bin/xxdiff"           => "-M +D+ +A+ +B+ +C+",
                # This one's even worse (no output filename).
                "/usr/bin/meld"             => "+A+ +B+ +C+",
                );

sub exec_cmd
{
  my @args=@_;
  my $CMD=$args[0];
  my @retData;
  my $i=0;

  open (FH,$CMD) || die("Can't '$CMD': $!\n");
  while($_=<FH>)
  {
    chomp($_);
    $retData[$i]=$_;
    $i++;
  }
  close(FH);

  return \@retData;

} # exec_cmd

sub diff_it
{
  my @args=@_;
  my $A=$args[0]; # mine
  my $B=$args[1]; # older
  my $C=$args[2]; # latest
  my $D=$args[3]; # output of merge (Destination)
  my @rdat;

  # What's is the diff of choice?
  if( $CHOSENDIFF eq "" )
  {
    # Glean our choice.
    diff_of_choice();
  }

  # $CHOSENDIFF has data.  We deal with the args.
  ($diffcmd,$diff_format)=(split /:/,$CHOSENDIFF);

  # This works.
  $diff_format=~s/\+A\+/$A/g;
  $diff_format=~s/\+B\+/$B/g;
  $diff_format=~s/\+C\+/$C/g;
  $diff_format=~s/\+D\+/$D/g;

  @rdat=@{exec_cmd("$diffcmd $diff_format 2>/dev/null |")};

  return @rdat;

} #diff_it

sub diff_of_choice
{
  foreach $diff_app (sort(keys(%DIFF3CMD_hash)))
  {
    if( ${diff_app}=~m/${DIFF3CMD}/o )
    {
      $CHOSENDIFF="$diff_app:$DIFF3CMD_hash{$diff_app}";
    }
  }
  if( $CHOSENDIFF eq "" )
  {
    # Big problem.  Some kind of disconnect w/the choice and the hash.
    # Most likely a typo.
    print "It would seem that the '${DIFF3CMD}' was not found in\n";
    print "the hash of diff applications I know about.  Please\n";
    print "investigate and correct.\n";
    exit(1);
  }

} #diff_of_choice

sub svn_update_info
{
  my @data;
  my @file_array;
  my $j;

  # Check to see if the d3cmdoverride is set so
  # we don't fail here if it wasn't.
  if( ${d3cmdoverride} eq "" )
  {
    $d3cmdoverride_final="";
  }
  else
  {
    $d3cmdoverride_final="--diff3-cmd=${d3cmdoverride}";
  }

  @data=@{exec_cmd("svn update ${d3cmdoverride_final} | grep ^C | awk '{print \$2}' |")};
  for($j=0;$j<(scalar(@data));$j++)
  {
    push( @file_array, exec_cmd("svn info $data[$j] |") );
  }

  return @file_array;

} # svn_update_info

sub parse_it
{
  my @file_array=@_;
  my @file;
  my $fname;
  my $fpath_tmp;
  my $fpath;
  my $file_ref;
  my $sbox_repo_base;
  my $sbox_repo_changed;
  my $sbox_repo_latest;
  my @cleanup_array;
  my @commit_array;
  my $rdat;
  my $retline;
  my $i;

  foreach $file_ref (@file_array)
  {
    @file=@{$file_ref};
    for($i=0; $i < (scalar(@file)-1);$i++)
    {
      if( $file[$i]=~m/Name:/o )
      {
        # Key off of "Name:" and then back up one to get "Path:".
        # This way the calculations will be correct when chopping off
        # the name portion on the path bit.
        $fname=(split /Name: /,$file[$i])[1];
        $fpath_tmp=(split /Path: /,$file[$i-1])[1];
        $fpath=(substr($fpath_tmp,0,(length($fpath_tmp)-(length($fname)+1))));
      }
      elsif( $file[$i]=~m/Conflict Previous Base File:/o )
      {
        $sbox_repo_base=(split /Conflict Previous Base File: /,$file[$i])[1];
      }
      elsif( $file[$i]=~m/Conflict Previous Working File:/o )
      {
        $sbox_repo_changed=(split /Conflict Previous Working File: /,
                            $file[$i])[1];
      }
      elsif( $file[$i]=~m/Conflict Current Base File:/o )
      {
        $sbox_repo_latest=(split /Conflict Current Base File: /,$file[$i])[1];
      }
    }

#   print "\n----------------------------------------------\n";
#   print "                      \$fpath: '$fpath'\n";
#   print "                      \$fname: '$fname'\n";
#   print "[A](mine) \$sbox_repo_changed: '$sbox_repo_changed'\n";
#   print "[B](older)   \$sbox_repo_base: '$sbox_repo_base'\n";
#   print "[C](latest)\$sbox_repo_latest: '$sbox_repo_latest'\n";
#   print "\n----------------------------------------------\n";

    # Send them in standard, ABCD, order.
    @rdat=diff_it("${fpath}/${sbox_repo_changed}",
                  "${fpath}/${sbox_repo_base}",
                  "${fpath}/${sbox_repo_latest}",
                  "${fpath}/${fname}");

    # Print out any return data.  Shouldn't be much of anything due to
    # the redirection to /dev/null in the invocation of whatever diff
    # command is used.
    print "\nOutput from diff3 command.\n";
    print "-----------------------\n";
    foreach $retline (@rdat)
    {
      print "$retline\n";
    }
    print "---------END-----------\n";

    # Add the files we may wish to remove to an array.  Keep *.mine
    # files in case something bad happened.
    push(@cleanup_array,
         "${fpath}/${sbox_repo_base}",
         "${fpath}/${sbox_repo_latest}",
         "${fpath}/${fname}.orig");

    # Make copies of *.mine for ctya purposes.
    if ( ${backup_mine_files} > 0 )
    {
      copy("${fpath}/${sbox_repo_changed}",
           "${fpath}/${fname}.${ENV{USERNAME}}");
    }

    # Return an array that contains all the files we might wish to
    # commit.
    push(@commit_array,"${fpath}/${fname}");
  }

  return \@cleanup_array, \@commit_array;

} #parse_it

sub clean_up
{
  my @args=@_;
  my @rdat;

  foreach $file (@{$args[0]})
  {
    unlink($file);
  }

  # Need to tell subversion we have resolved the conflicts.
  @rdat=@{exec_cmd("svn -R resolved . |")};
  print "\nOutput from subversion 'resolved' command.\n";
  print "-----------------------\n";
  foreach $line (@rdat)
  {
    print "$line\n";
  }
  print "---------END-----------\n";

} #clean_up

sub main
{
  my @args=@_;
  my $cleanup_aref;
  my $commit_aref;
  my $file;

  ($cleanup_aref,$commit_aref)=parse_it(svn_update_info());
  clean_up($cleanup_aref);

  print "\nDon't forget to commit these files:\n";
  print "-----------------------\n";
  foreach $file (@{$commit_aref})
  {
    print "$file\n";
  }
  print "---------END-----------\n";

} #main

# Special global.
$CHOSENDIFF="";

# Get the ball rolling.
main(@ARGV);