#!/opt/perl/bin/perl -w
$versn = "21.1 - 19 Jun 2004";            # Code version and modify date
#
# Compare two directory trees
# (with appologies to Apollo Domain/OS's 'cmt' command)
#
# For help and a description of the use of this script, run:
#
#   cmtree --help
#
# This code copyright 1999-2004 by
# D. W. Eaton, Artronic Development, Phoenix, AZ -- dwe@arde.com
#
# This software is made freely available under the provisions of the Perl
# "Artistic" license:  http://language.perl.com/misc/Artistic.html
#
# This code is not supported and is not warranteed to perform any particular
# function. Contact dwe@arde.com for aditional information.
# If you find bugs or make enhancements, it would be appreciated if you
# sent them on to the author at dwe@arde.com.
#
use Getopt::Long;
use File::Find;
#
# Constants
$true = 1; # Truth values
$false = 0;
$all   = 2;  # for Getopt ignore case
use vars qw($false $true $all);
#
# Option defaults
$opt{'completecompare'} = $false;
$opt{'content'}     = $false;
$opt{'help'}        = $false;
$opt{'ignoreat'}    = $false;
$opt{'ignorebak'}   = $false;
$opt{'ignoredir'}   = $false;
$opt{'ignoretarg'}   = $false;
$opt{'list'}        = $false;
$opt{'listdir'}     = $false;
$opt{'listfile'}    = $false;
$opt{'quiet'}       = $false;
$opt{'verbose'}     = $false;
$opt{'veryverbose'} = $false;
# Migration extensions:
$opt{'allver'}      = $false;
$opt{'dseedir'}     = $false;
$opt{'gendir'}      = $false;
$opt{'swapdollar'}  = $false;
$opt{'vob'}         = '';
#
# Initialize
$scriptleaf = $0;
$scriptleaf =~ s/^.*\///; # strip leading path to see who we are
#
$outputOK = $true;   # Show normal output lines
$cntarg = 0;         # Total arguments provided
$rawtotal = 0;       # Total paths checked
$miscomparetot = 0;  # Count miscompares
$missingvob = 0;     # Count missing VOB files
$comparefailtot = 0; # Count failures
$compareok = 0;      # Count successful compares
$startdir = `pwd`; # Find out where we were when we started
chomp ($startdir);
# ---------------------- logic -------------------------
#
# Process command-line
@opts = qw( list|l
            listfile|lf
            listdir|ld
            abterr|ae
            content|c
            completecompare|cc
            dseedir|d
            allver|av
            gendir|g
            vob=s
            swapdollar|sd|s
            ignoreat ignorebak ignoredir ignoretarg
            help|h quiet|q verbose|v veryverbose|vv );

$Getopt::Long::bundling = $true;  # perl 5.003 and earlier will complain about this
$Getopt::Long::ignorecase = $all;
GetOptions (\%opt, @opts);

if ($opt{'list'})
{
 # Then some other values are implied
 $opt{'listfile'} = $true;
 $opt{'listdir'} = $true;
}

if ($opt{'veryverbose'})
{
 # Then some other values are implied
 $opt{'verbose'} = $true;
}

if ($opt{'completecompare'})
{
 # Force content compare if complete compare requested
 $opt{'content'} = $true;
}

if ($opt{'help'})
{
   &syntax_message ();
   exit (1);
}

#
# Verify required options are present, and other checks

# - - -
# So, should still have more arguments, must be filename(s)
  # - - - - - - Do the work - - - -
# Find clearcase incase it is needed:
if (-x "/opt/atria/bin/cleartool")
{
 $cleartoolpath = "/opt/atria/bin/cleartool"; # Pathname to cleartool
}
else
{
 $cleartoolpath = "cleartool"; # Hope that tool is on the PATH
}

  ($thisdate,$thistime) = &getcurtime; # Get current date/time

  unless ($opt{'quiet'})
  {
   print "[$thisdate.$thistime] $scriptleaf version $versn\n";
   if ($opt{'content'})
   {
    print "Content will be compared";
    if ($opt{'completecompare'})
    {
     print " and results of compare will be shown";
    }
    if ($opt{'vob'})
    {
     print ", using VOB: $opt{'vob'}"
    }
    print "\n";
   }
  }


  $argerr = 0; # No argument errors yet

  # Now look for arguments:
  while (scalar (@ARGV))
  {
   $nxtarg = shift (@ARGV) ;
   if ($nxtarg &&
       -d "$nxtarg")
   {
    # Process next argument
    $cntarg++;
    print "Begin searching directory argument '$nxtarg' ($cntarg)\n" if ($opt{'verbose'});
    if (! $argerr)
    {
     # Don't bother gatherng names if we already found a fatal error:
     $allargs{$cntarg} = $nxtarg; # Show we've been here
     $firstdir = `pwd`; # Find out where we are
     chomp ($firstdir);
     find (\&getfiles,"$nxtarg");
    }
   }
   else
   {
    print "ERROR: Unable to find directory '$nxtarg'\n";
    $argerr++;
   }
  }

  # See if we can continue:
  if ($argerr ||
      ! $cntarg ||
      ($cntarg == 1 && !$opt{'vob'}) ||
      ($cntarg == 2 && $opt{'vob'}) ||
      $cntarg > 2)
  {
   # Guess not ... we can't compare just one thing and we aren't
   # smart enough to compare more than 2 things ...
   print STDERR "Unable to continue";
   if ($argerr)
   {
    print STDERR " - $argerr directories not found";
   }
   if (! $cntarg ||
       ($cntarg == 1 && !$opt{'vob'}))
   {
    print STDERR " - must have 2 arguments to compare, had $cntarg";
   }
   if ($cntarg == 2 && $opt{'vob'})
   {
    print STDERR " - must have 1 argument with --vob option, had $cntarg";
   }
   if ($cntarg > 2)
   {
    print STDERR " - must have only 2 arguments to compare, had $cntarg";
   }
   print STDERR "\n";
   if (! $cntarg)
   {
    &syntax_message ();
   }
  }
  else
  {
   if ($opt{'vob'})
   {
    # Ah, have the other compare dir identified in a special way:
    if (-d "$opt{'vob'}")
    {
     $cntarg = 2;
     print "Begin searching VOB argument '$opt{'vob'}' ($cntarg)\n" if ($opt{'verbose'});
     $allargs{$cntarg} = $opt{'vob'}; # Show we've been here
     $firstdir = `pwd`; # Find out where we are
     chomp ($firstdir);
     # Go get the list of files/dirs down the VOB tree, too
     find (\&getfiles,"$opt{'vob'}");     
    }
    else
    {
     print STDERR "Specified VOB path '$opt{'vob'}' not found, aborting.\n";
     exit (1);
    }
   }

   &checkfiles(1);
   &checkfiles(2);

   # Now see what we have ...
   print "Entries not found";
   if ($opt{'content'})
   {
    print " and compare errors";
   }
   print ":\n";
   $cntmissing = 0; # Count of missing files
   $cntrename = 0;  # Count of renamed files
   $cntignore = 0;  # Count of ignored files
   $cntmatch = 0;   # Count match
   $cntver = 0;     # Count of versions considered
   undef $cntvertree; # Clear old counts
   foreach $key (sort keys %foundnames)
   {

    # See if this is a special compare which should eliminate certain constructs
    # (these are old DSEE things we don't want to compare)
    if ($opt{'gendir'} ||
        ($key !~ /^doc\/gen/ &&
         $key !~ /^pub\/gen/))
    {
     # OK to process this name ...
     $newkey = $key; # Pattern to use in new directory (usually the same)
     $oldkey = $key; # Pattern to use in old directory (usually the same)
     if ($key =~ /\$/)
     {
      # Ah, the pathname contained a "$" ... swap it for a "_"
      $newkey =~ s/\$/_/g;
     }
     if ($key =~ /_/)
     {
      # Ah, the pathname contained a "_" ... swap it for a "$"
      $oldkey =~ s/_/\$/g;
     }
     if (defined $foundnames{$key}[1] &&
         defined $foundnames{$key}[2] &&
         $foundnames{$key}[1] eq $foundnames{$key}[2])
     {
      # Found an exact match on name and item type
      $cntmatch++;
      # See if we should compare content
      if ($opt{'content'} &&
          $foundnames{$key}[1] eq 'File')
      {
       # Go compare the content
       &comparecontent("$allargs{'1'}/$key","$allargs{'2'}/$key");
      }
     }
     else
     {
      if (defined $foundnames{$key}[1] &&
          ! defined $foundnames{$key}[2])
      {
       if ($opt{'swapdollar'} &&
           (defined $foundnames{$newkey}[2] ||
           defined $foundnames{$oldkey}[2]))
       {
        # Found a renamed one ...
        $cntrename++;
        if ($opt{'content'})
        {
         # Go compare the content
         &comparecontent("$allargs{'2'}/$oldkey","$allargs{'1'}/$newkey");
        }
       }
       else
       {
        $combopath = $allargs{'2'} . '/' . $key;
        # Recognize key directory names as the start of VOB versioned files:
        if ($opt{'vob'} &&
            ($combopath =~ /\/(src|hlp|abs|gdf|gdfregl|gdfpt|tst2|tst1|tst3)\// ||
             $combopath =~ /^(src|hlp|abs|gdf|gdfregl|gdfpt|tst2|tst1|tst3)\//) &&
            $key =~ /\/\d+$/)
        {
         $leadpath = $combopath;
         $combopath =~ s/^.*\/(src|hlp|abs|gdf|gdfregl|gdfpt|tst2|tst1|tst3)\///;
         $combopath =~ s/^(src|hlp|abs|gdf|gdfregl|gdfpt|tst2|tst1|tst3)\///;
         $leadpath =~ s/$combopath$//;
         $leadpath =~ s/\/$//;
         ($fileleaf,$filever) = split ("\/",$combopath,2);
         # If /main/ or @@ already there, do not add them, doing a vob-to-vob-image
         if ($filever =~ /^\d+$/)
         {
          if ($filever =~ /^main\//)
          {
           $filever =~ s/^/\//;
          }
          else
          {
           $filever =~ s/^/\/main\//;
          }
         }
         else
         {
          # So why did we ask this question?
          if ($filever =~ /^main\//)
          {
           $filever =~ s/^/\//;
          }
          else
          {
           $filever =~ s/^/\/main\//;
          }
         }
         $ccaseVOBfile = "$leadpath/$fileleaf";
         unless ($ccaseVOBfile =~ /\@\@$/)
         {
          $ccaseVOBfile .= "\@\@";
         }
         $ccaseVOBfile .= "$filever";
         if ($opt{'ignoreat'})
         {
          $ccaseVOBfile =~ s/\@\@.*$//;
         }
         if (-f $ccaseVOBfile)
         {
          if ($opt{'content'})
          {
           # Go compare the content
           &comparecontent("$allargs{'1'}/$key","$ccaseVOBfile");
          }
         }
         else
         {
          print " Missing VOB file: $ccaseVOBfile\n";
          $missingvob++; # bump count
         }
        }
        else
        {
         $cntmissing++;
         $foundpath = "$allargs{'1'}/$key";
         print " Missing2 $foundnames{$key}[1]:" if ($opt{'verbose'});
         if (! $opt{'ignoredir'} ||
              (! -d "$foundpath") || ($opt{'verbose'}))
         {
          print " $allargs{'2'}/$key\n";
         }
         exit (1) if ($opt{'abterr'});
        }
       }
      }
      if (defined $foundnames{$key}[2] &&
         ! defined $foundnames{$key}[1])
      {
       if ($opt{'swapdollar'} &&
           (defined $foundnames{$newkey}[1] ||
           defined $foundnames{$oldkey}[1]))
       {
        # Found a renamed one ...
        $cntrename++;
       }
       else
       {
        if (! $opt{'ignoretarg'})
        {
         $cntmissing++;
         print " Missing $foundnames{$key}[2]:" if ($opt{'verbose'});
         print " $allargs{'1'}/$key\n";
         exit (1) if ($opt{'abterr'});
        }
       }
      }
     }
    }
    else
    {
     # Ignore it, but count it
     $cntignore++;
    }
   }

   # $total = scalar (%foundnames);
   print "Total paths checked: $rawtotal\n";
   if ($cntver)
   {
    print "(Total versions considered: $cntver)\n";
   }
   print "Paths found below:\n";
   foreach $key (sort keys %totcnt)
   {
    print " [$key] $allargs{$key}: $totcnt{$key} of $rawtotcnt{$key}\n";;
    if (defined $cntvertree{$key})
    {
     print " (versions: $cntvertree{$key})\n";
    }
   }

   if ($cntmatch)
   {
    print "Had $cntmatch matched names\n";
   }
   if ($cntmissing)
   {
    print "Discovered $cntmissing mismatched names\n";
   }
   else
   {
    print "(None - directories matched 'well enough')\n";
   }
   if ($cntrename)
   {
    print "Found $cntrename renamed names\n";
   }
   if ($cntignore)
   {
    print "Ignored $cntignore names\n";
   }
   if ($miscomparetot)
   {
    print "Miscompared $miscomparetot file pairs";
    if ($compareok)
    {
     print ", $compareok compared OK";
    }
    print ".\n";
   }
   elsif ($opt{'content'})
   {
    print "No miscompared content detected";
    if ($compareok)
    {
     print ", $compareok compared OK";
    }
    print ".\n";
   }
   if ($missingvob)
   {
    print "Encountered $missingvob missing VOB files (may be branch name changes)\n";
   }
   if ($comparefailtot)
   {
    print "Failed to compare $comparefailtot file pairs\n";
   }
  }
  # Terminate
  ($thisdate,$thistime) = &getcurtime; # Get current date/time
  print "[$thisdate.$thistime] Done.\n" unless ($opt{'quiet'});
  # - - - - - - Work done - - - -
# - - -

#
# All done
if ($outputOK)
{
 print "\n" unless ($opt{'quiet'});
}

exit (0);
### END.

# --------------------------- subroutines ----------------------------
# ----------
# Get current date/time in format yy/mm/dd hh:mm:ss
# ($thisdate,$thistime) = &getcurtime;
sub getcurtime
{
 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst);
 my ($thisdate,$thistime);
 ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
 $mon++;
 $wday++;
 # Some flakey Y2K stuff:
 if ($year < 69)
 {
  $year += 100;  # Bias year to add century if it wasn't there and is before 1969
 }
 if ($year < 1900)
 {
  $year += 1900; # Bias year to add centuries if it wasn't already there
 }
 $thisdate = sprintf ("%02.0f/%02.0f/%02.0f",$year,$mon,$mday);
 $thistime = sprintf ("%02.0f:%02.0f:%02.0f",$hour,$min,$sec);
 return ($thisdate,$thistime);
}
# ----------
# &comparecontent("filename1","filename2");
sub comparecontent
{
 my ($name1,$name2) = @_;
 my ($mydir, $compdiff, $compfail);

 if ($opt{'ignoreat'})
 {
  $name1 =~ s/\@\@.*$//;
  $name2 =~ s/\@\@.*$//;
 }
 if (-f $name1 && -f $name2)
 {
  print "Comparing '$name1' -> '$name2' ...\n" if ($opt{'verbose'});
  $compdiff = $false;
  $compfail = $false;

   if (! open (CMP, "diff 2>&1 '$name1' '$name2'" . "|"))
   {
    print STDERR "Cannot access: diff '$name1' '$name2'\n";
   }
   else
   {
    # Ok, opened fine ... get back the results
    print " Results from diff '$name1' '$name2'\n" if ($opt{'veryverbose'});
    $lineno = 0; # Results line number
    while (defined($line=<CMP>))
    {
     chomp ($line);
     if (! $lineno &&
         $line =~ /No such file or directory/)
     {
      $compfail = $true; # Failed
     }
     else
     {
      $compdiff = $true;
     }
     if ($compdiff && ! $lineno)
     {
      $miscomparetot++; # Count miscompares
      print " Compare diff: '$name1' -> '$name2'\n";
     }
     print " $lineno: " if ($opt{'veryverbose'} || $opt{'completecompare'});
     print "$line\n" if ($opt{'veryverbose'} || $opt{'completecompare'});
     $lineno++;
     # push (@results, $line);
    }
    close (CMP);
   }
  if ($compfail)
  {
   $comparefailtot++; # Count failures
   print " Compare fail: '$name1' -> '$name2'\n";
  }
  else
  {
   $compareok++; # Count successful compares
   print " Compare done\n" if ($opt{'verbose'});
  }
 }
 else
 {
  $comparefailtot++; # Count failures
  print STDERR "Oops, unable to find both files: '$name1' and '$name2'\n";
  if (! -f "$name1")
  {
   print "ERROR: not a file: '$name1'\n";
  }
  if (! -f "$name2")
  {
   print "ERROR: not a file: '$name2'\n";
  }
 }
 return;
}
# ----------
# &getfiles,"$dirpath"
# Get names of all files down path
sub getfiles
{
   my ($nextname, $nextfilename, $checknext, $thisdir);
   my ($nextleaf);

   $nextname = $File::Find::name;
   $thisdir = `pwd`; # Find out where we are now
   chomp ($thisdir);
   $nextleaf = $nextname;
   $nextleaf =~ s/^$allargs{$cntarg}//; # Strip start dir
   $nextleaf =~ s/^\///; # Strip start dir slash if any
   $rawtotcnt{$cntarg}++; # count the raw entries we found here
   $rawtotal++;
   if ($nextleaf)
   {
    if (! ($opt{'ignorebak'} &&
           ($nextleaf =~ /\.bak$/ ||
            $nextleaf =~ /\~$/)))
    {
     # OK, not an ignored backup file ...
     if ($opt{'dseedir'} ||
         ($nextleaf !~ /\$\.history\.\$/ &&
         $nextleaf !~ /\$\.tasks\.\$/ &&
         $nextleaf !~ /\$\.tasklist\.\$/ &&
         $nextleaf !~ /\$\.master_tasklist\.\$/ &&
         $nextleaf !~ /\$\.protection\.\$/ &&
         $nextleaf !~ /\$\.semaphore\.\$/ &&
         $nextleaf !~ /\$\.database\.\$/ &&
         $nextleaf !~ /^rls\// &&
         $nextleaf !~ /^sys\//))
     {
      # Either it wasn't a special DSEE file or we didn't care that it was
      $totcnt{$cntarg}++; # OK, count one we need to process
      #
      $foundnames{$nextleaf}[$cntarg] = 'xx';
     }
    }
   }
}
# ----------
# &checkfiles($cntarg)
# Check names of all files found for specified arg
sub checkfiles
{
   my ($cntarg) = @_;

   my ($nextname, $nextfilename, $checknext, $thisdir);
   my ($nextleaf, $key);
   my ($fileleaf,$filever,$altarg);

   print "Checking '$allargs{$cntarg}'\n" if ($opt{'verbose'});
   $thisdir = `pwd`; # Find out where we are now
   chomp ($thisdir);

   foreach $key (sort keys %foundnames)
   {
    if (defined $foundnames{$key}[$cntarg])
    {
     $checknext = $allargs{$cntarg} . '/' . $key;

     if (-d "$checknext")
     {
      print " Dir:  $checknext\n" if ($opt{'verbose'} || $opt{'listdir'});
      $foundnames{$key}[$cntarg] = 'Directory';
     }
     elsif (-f "$checknext")
     {

      if ($opt{'allver'})
      {
         if (! open (CT, "$cleartoolpath lsvtree -s -all $checknext 2>&1" . "|"))
         {
          print STDERR ("Cannot access cleartool - $!\n");
         }
         else
         {
          # Ok, opened fine ... get back the results
          while (defined($line=<CT>))
          {
           chomp ($line);
           if (-f "$line")
           {
            $cntver++; # Bump number of versions considered
            $line2 = $line;
            $line2 =~ s/^.*\/$key\@\@/$key\@\@/;
            $line2 =~ s/^$allargs{$cntarg}\///; # Remove the leading path stuff if there
            if (! defined $foundnames{$line2}[$cntarg])
            {
             $cntvertree{$cntarg}++; # versions considered in this tree
             $foundnames{$line2}[$cntarg] = 'File';
            }
           }
          }
          close (CT);
         }
      }
      print " File: $checknext\n" if ($opt{'verbose'} || $opt{'listfile'});
      $foundnames{$key}[$cntarg] = 'File';
     }
     elsif (-l "$checknext")
     {
      print " Link: is $checknext\n" if ($opt{'verbose'} || $opt{'listfile'});
      $foundnames{$key}[$cntarg] = 'Link';
     }
     elsif (-e "$checknext")
     {
      print " Found sd'$startdir':fd'$firstdir':'$allargs{$cntarg}' - cd'$thisdir' $checknext '$key'\n" if ($opt{'verbose'} || $opt{'listfile'});
      $foundnames{$key}[$cntarg] = 'Other';
     }
     else
     {
      print " Looked for sd'$startdir':fd'$firstdir':'$allargs{$cntarg}' - cd'$thisdir' $checknext '$key'\n" if ($opt{'veryverbose'} || $opt{'listfile'});
      $foundnames{$key}[$cntarg] = 'Missing';
     }
    }
   }
}
# --------------------
#
# Print syntax message
sub syntax_message
{
   my ($progname);

   chomp ($progname = `basename $0`);
   print STDERR  qq
Compare two directory trees

Compare two directory trees and report the files in one
that are missing from the other. (Contents of the files
are not compared unless --content option used.)
(with appologies to Apollo Domain/OS's 'cmt' command
which spawned the idea for this script)

Syntax:

   $progname  [ options ] [pathname(s)]

Where 'options' are:

   -l, --list
       List all directories and files compared.
   --ld, --listfile
       List all directories compared.
   --lf, --listdir
       List all files compared.
   --ae, --abterr
       Abort on the first mismatch, or if the source tree contains a
       name not found in the target tree.  By default, the comparison
       continues after the mismatch is reported.

   -c, --content
       Content of files with matching file names is compared as well
   --cc, --completecompare
       List compare results in output (implies -c)
   -d, --dseedir
       Process files in directories recognized as DSEE housekeeping
       directories: the \$.xxx.\$ directories in libraries, and the
       rls and sys directories.
       By default, files in these directories are ignored.
   --av, --allver
       Expand paths within a VOB so that all versions are considered
   -g, --gendir
       Process files in directories known by standard conventions to
       contain generated files (doc/gen and pub/gen).
       By default, files in these directories are ignored.
   --ignoreat
       Ignore at signs and all that follows in a VOB path.
   --ignorebak
       Ignore recognizable backup files files which end with .bak or ~
   --ignoredir
       Ignore directories defined under the first (or non-VOB) argument, but
       which are not defined under the second argument. (Allows for ignoring
       branch name directories, etc. in import trees.)
   --ignoretarg
       Ignore file names which exist in the target tree but not in the source
       tree.
   -s, --swapdollar
       Swap dollarsigns (\$) in file names for underscores (_) and
       assume a match if the alternate is found. (This is an
       extension to accomodate migration from Apollo to HP-UX.
       By default, this transformation is not made.)
   --vob=path
       Path to a directory within a ClearCase VOB to which the burst
       version files in the directory defined by the first (and only)
       pathname argument will be compared

   -h, --help
       Print this help message
   -q, --quiet
       Do not show supporting info lines, just results
   -v, --verbose

Output is to STDOUT, errors preventing operation are sent to STDERR.

EXAMPLES
--------

 Compare names in directory foo to names in directory bar:

   $progname  foo bar

 Compare names in directory foo to names in VOB bar
 (foo is in clearexport DSEE version format):

   $progname  foo --vob=bar

 Compare content of directory foo to content of VOB bar
 (foo is in clearexport ClearCase version format):

   $progname  foo --vob=bar --content

 Compare content of directory foo to content of VOB bar and show differences
 (foo is in clearexport ClearCase version format):

   $progname  foo --vob=bar --completecompare

 Compare content of VOB foo to content of VOB bar and show differences
 for all versions:

   $progname  foo bar --completecompare --allver

;
}
