# -*-perl-*-
#
#   Copyright (c) 1996 Network Appliance, Inc.
#   Copyright (c) 2002 PiroNet NDH AG
#
#   You may distribute under the terms of the Artistic License, as
#   specified in the README file included in the cvslines
#   distribution.
#
#   Original Author: Richard Geiger for Network Appliance, Inc.
#   Adaption to cvs >=1.11 by Juergen Jatzkowski and Ingo Rockel
#
#  $Id: cvslines-commit,v 1.3 2005/10/11 08:14:31 irockel Exp $

#  This file is used by "cvslines".
#  It is not intended for standalone execution.

$Logfrom = "commit";

#  The following variables are global for this stuff:
#
#    $Working_Line_Spec  # what the working line is originally set to
#    $Cur_Line_Spec	 # what the current working line is currently set to
#    $First_Commit       # whether we have yet done the first (of possibly
#                        # multiple, for this file) commit
#
#  Turns out we need to keep "$Cur_Line_Spec" only to know when we
#  need to restore it at the end for a given file, because none of the
#  other operations need it; the "straight (i.e., non-merge) commits
#  assume that the version in the working dir is what should be
#  commited, and they always get done before any of the merge
#  ones. The merge ones don't assume anything, since they always check
#  out the spec line first, in order to prepare for the commit.
#  


sub report_conflicts
{
  if ($Batch && $Merge_conflicts ne "")
    {
      print TTYO
        "\nThe following merge conflicts were detected:\n$Merge_conflicts\n";
    }
}


sub commit_cleanup
{
  unlink $Logmsgfile;
  #  More cleanup TBD
  #
  &report_conflicts;
  exit 1;
}


# (For debugging...)
#
if (1)
  { $retmsg = " [press Return to]"; $unretnl = ""; }
else
  { $retmsg = ""; $unretnl = "\n"; }

#  functions unique to $Mynamebase commit
#
sub do_system
{
  my ($cmd, $term_on_fail) = @_;
  my $status;

  if (! defined($term_on_fail)) { $term_on_fail = 1; }

  if ((! $Noaskexec) && (! $Noexec))
    {
      print TTYO "$Myname:$retmsg> $cmd$unretnl";
      if ($retmsg) { &wait_return(); }
    }
  else
    { print TTYO "$Myname> $cmd\n"; }
  if ($Noexec) { return 0; }

  $status = system $cmd;
  &log("\"$cmd\" returned $status");
  if ($status && $term_on_fail) { &commit_cleanup(); }
  return;          
}


$Logmsgfile = "$Userhome/.$Myname.logmsg";

sub get_logmsg
{
  my ($file, $rev) = @_;

  if (! defined ($msg = &get_RCS_log($file, $rev)))
    { $Logmsgopt = ""; return; } # Well, she'll have to retype it.
      
  #  Now stash the log message in a file...
  #
  if (! open(LOG, ">$Logmsgfile"))
    { $Logmsgopt = ""; return; }
  printf LOG "%s", $msg; close LOG;
  $Logmsgopt = " -F $Logmsgfile";
}


sub before_first_commit
{
  my ($file) = @_;
  my $m;

  # OK, let's 
  #

  @Entry = grep(/^\/$file\//, @CVS_Entries);
  if ($#Entry != 0)
    {
      $m = "internal error: found $#Entry entries for \"$file\".";
      print TTYO "$Myname: $m\n"; &log($m); &commit_cleanup();
    }

  ($dummy, $Ent_Name, $Ent_Rev, $Ent_Time, $Ent_Opts, $Ent_Tag)
     = split(/\//, $Entry[0]);

  $Ent_Tag =~ s/^T//;
  if ($Ent_Tag eq "") { $Ent_Tag = "head"; }
  $Working_Line_Spec = $Ent_Tag;
  $Cur_Line_Spec = $Working_Line_Spec;
}


sub after_first_commit
{
  my ($file, $rev, $currev) = @_;

  if ((! (-f $Logmsgfile && -r $Logmsgfile)) && (! $Noexec))
    { &get_logmsg($file, $rev); } #  Get the log message for possible reuse

  # set merge -j specs for any subsequent merges
  #
  if ($Ent_Rev ne "0") { $j1 = $currev; $j2 = $rev; }

  $First_Commit = 0;
}


sub commit
{
  my ($file, $spec, $rev) = @_;
  my $ropt;
  my $branchtag;
  my $rootrev;
  my $currev;

  print TTYO "$Myname: committing new revision $rev...\n";

  if ($rev =~ /^(.+):([0-9.]+)$/)
    {
      #  Handle this special "spoof-CVS" case, wehre we really
      #  want to commit to the next rev on an existing RCS branch,
      #  rather than causing a new RCS branch (as CVS would)...
      #
      $branchtag = $1; $rev = $2;
      $rev =~ /^(.+)\.[0-9]+\.[0-9]+$/;
      $rootrev = $1;
      print TTYO "$Myname: first, spoof cvs with a branch tag update...\n";
      &branchtag($file, $branchtag, $rootrev);
      print TTYO "$Myname: next, spoof the cvs up-to-date check...\n";
      &do_system("cvs update $file"); # required for inane up-to-date-check
    }

  if ($First_Commit) { &before_first_commit($file); }

  if (! (($Ent_Rev eq "0" && $Ent_Time !~ /^... ... .. ..:..:.. ....$/)))
    { if (! &set_RCS_revs($CVS_Repository, $file)) { &commit_cleanup(); } }

  if ($Logmsg eq "ask")
    {
      if (-f $Logmsgfile && -r $Logmsgfile)
        {
          $themsg = &slurp($Logmsgfile);
          print TTYO <<LIT;
$Mynamebase:

Re-use the previous log message:

$themsg

LIT
          $ans = &ask("", "n", "y", "n");
          if ($ans eq "y")
            { $Logmsgopt = " -F $Logmsgfile"; } else { $Logmsgopt = ""; unlink "$Logmsgfile"; }
        }
    }

  if ($spec eq "this")
    {
      $currev = &rev_on_line($Working_Line_Spec);
      &do_system("cvs commit$Logmsgopt $file");
    }
  else
    {
      #  we're commiting onto a different line
      #
      if ($Ent_Rev eq "0")
        {
          #  This is a newly added file - so we need to play a different game.
          #  (It must be going onto the "head", because it's not a "this",
          #  and the head is the only alternate place it *could* go!)
          #
          print TTYO "$Myname: fool cvs into doing the right thing (don't panic!)...\n";
          $currev = &rev_on_line($head);
          &do_system("/bin/mv -f CVS/Tag CVS/Tag-");	    # we'll pretend we're on a head line
          &do_system("/bin/mv -f $file $file.cvslines.$$.save"); # stash the working file
          &do_system("cvs remove $file"); 		    # cause we need to...
          &do_system("/bin/mv -f $file.cvslines.$$.save $file"); #  (un-stash the working file)
          &do_system("cvs add $file");	 		    # ...re-add it (on the head)
          &do_system("cvs commit$Logmsgopt $file");	    #
          &do_system("/bin/mv -f CVS/Tag- CVS/Tag");        # drop the pretense
          $Cur_Line_Spec = "head";			    # to trigger &restore_line() later
        }
      else
        {
          $currev = &rev_on_line($spec);

          &do_system("/bin/mv -f $file $file.cvslines.$$.save"); #  Stash the working file
      
          #  Check out the line we're actually committing to
          #
          if ($spec eq "head") {$ropt = "-A"; } else { $ropt = "-r $spec"; }
          &do_system("cvs update $ropt $file");
          $Cur_Line_Spec = $spec;

          &do_system("/bin/mv -f $file.cvslines.$$.save $file"); #  Get the working file back...

          &do_system("cvs commit$Logmsgopt $file")               #  And do the commit...
        }
    }

  if ($First_Commit) { &after_first_commit($file, $rev, $currev); }
}


sub merge
{  &mergeit(@_, 1, 1); }


#  Assumes nothing about the working version of the file in the
#  working directory (always does a cvs update -r <spec>).
#  Can also optionally skip the merge and/or commit part.
#
sub mergeit
{
  my ($file, $spec, $rev, $do_merge, $do_commit) = @_;
  my $ropt;

  print TTYO "$Myname: merging to new revision $rev...\n";

  #  Check out the line we're merging to
  #
  if ($spec eq "head") { $ropt = "-A"; } else { $ropt = "-r $spec"; }
  if ($do_merge) { $ropt = "-k k $ropt"; }
  &do_system("cvs update $ropt $file; /bin/sleep 2");
  $Cur_Line_Spec = $spec;

  if ($do_merge)
    {
      #  Do the cvs update to merge...
      #    (Make this more intelligent TBD - i.e., differentiate between
      #    "can't automerge" and other failures?)
      #
      $cmd = "cvs update -k k -j $j1 -j $j2 $file";

      $conflicts = 0;

      if ((! $Noaskexec) && (! $Noexec))
        {
          print TTYO "$Myname:$retmsg> $cmd$unretnl";
          if ($retmsg) { &wait_return(); }
        }
      else
        { print TTYO "$Myname> $cmd\n"; }

      if (! $Noexec)
        {
          if (! open(UPD, "$cmd 2>&1|"))
            {
              $m = "open \"$cmd 2>&1 |\" failed. commit terminated.";
              print TTYO "$Myname: $m\n"; &log($m); &commit_cleanup();
            }    

          while (<UPD>)
            {
              print TTYO $_;
              if (/conflict/) { $conflicts = 1; }
            }  
          $status = $?;
          &log("\"$cmd\" returned $status");
        }

      if ($conflicts)
        {
          if ( !$Batch)
            {
              print TTYO "$Myname: merge conflicts detected.\n";
              @choices = ("$Myname: skip or edit", "e", "s", "e");
            }
          while (1)
            {
              if ($Batch) { $ans = "s"; } else { $ans = &ask(@choices); }
              @choices = ("$Myname: commit, skip or edit", "c", "c", "s", "e");
              if ($ans eq "c") { last; }
              if ($ans eq "e") { &do_system("$editor $file", 0); next; }
              &do_system("/bin/rm -f $file-conflicts_$spec; cp $file $file-conflicts_$spec");
              print TTYO "$Myname: (merge file saved as \"$file-conflicts_$spec\").\n";
              my $dir;
              $dir = `/bin/pwd`; chop $dir;
              $Merge_conflicts .= "$dir/$file-conflicts_$spec\n";
	      $skiprevs{$rev} = 1;
              return;
            }
        }
    }

  if ($do_commit)
    {
      #  Maybe allow user to choose to give a different log message?
      #  (or only if automarge failed?) FTD

      if ($First_Commit) { &before_first_commit($file); }
      &do_system("cvs commit$Logmsgopt $file");
      if ($First_Commit) { &after_first_commit($file, $rev, $currev); }
    }
}


sub branchtag
{
  my ($file, $spec, $rev) = @_;

  print TTYO "$Myname: set branch tag \"$spec\" to revision $rev...\n";

  &do_system("cvs tag -B -F -r $rev -b $spec $file");
}


sub restore_line
{
  my ($file) = @_;

  if ($file ne "" && $Working_Line_Spec ne $Cur_Line_Spec)
    {
      print TTYO "$Myname: restoring working tag to \"$Working_Line_Spec\"...\n";
      if ($Working_Line_Spec eq "head")
        { $ropt = "-A"; } else { $ropt = "-A -r $Working_Line_Spec"; }
      &do_system("/bin/rm -f $file; cvs update $ropt $file");
    }
}


sub commit_dir
{
  my ($dir) = @_;
  my $here;
  my $CVS;
  my $file;
  my $path;
  my $ropt;
  my $op;
  my $spec;
  my $rev;

  chdir($dir) || die; ###

  $here = `/bin/pwd`; chop $here;

  print TTYO "$Myname: executing plans for directory \"$here\"...\n";

  if ($Logmsg eq "ask")
    {
      $Logmsgopt = " -m '$Myname.$$'"; # for debug so we don't have to bother TBD
      $Logmsgopt = "";
    }

  $CVS = "$here/CVS";

  #  Initialize globals used by the $Myname commit command
  #
  if (! &module_config($here))
    {
      print TTYO "$Myname: no $Mynamebase.config file for commit in \"$here\".\n";
      chdir $Here;
      return 0;
    }

  if (!open(PLAN, "<$Planfile"))
    {
      print TTYO "$Myname: can't open \"$Planfile.plan\" to read\n";
      chdir $Here;
      return 0;
    }

  $file = $path = "";

  while (<PLAN>)
    {
      #  Look for a "path" directive...

      if (/^$|^\s*#/) { next; }
      elsif (/^\s*path\s+([^\s]+)/)
        {
          # restore file working copy to original line
          # (if needed)
          #
          &restore_line($file);
          ($file = ($path = $1)) =~ s%^.*/%%;
          $First_Commit = 1; # next commit will be the first one for this file.
          undef $msg;
          undef %skiprevs;

          print TTYO "$Myname: executing plan for file $path...\n";
          next;
        }
#      elsif (/^\s*(commit|merge|branchtag|commitTag|mergeCommitTag)\s+([^\s]+)\s+([^\s]+)\s+/)
      elsif (/^\s*(commit|merge|branchtag)\s+([^\s]+)\s+([^\s]+)\s+/)
        {
          $op = $1; $spec = $2; $rev = $3;
          if (defined($skiprevs{$rev}))
            { print TTYO "$Myname: skipping action for line spec \"$spec\".\n"; }
          else
            { &$op($file, $spec, $rev); }
          next;
        }
      $m = "unimplemented plan directive: <$_>";
      print TTYO "$Myname: $m\n"; &log($m);
    }

  &restore_line($file);

  unlink "$Planfile";
  chdir $Here;
}


sub cvstat
{
  my ($dir, $normdir, $file) = @_;
  my $status = 0;

  if (defined($file))
    { $cmd = "cvs status $dir/$file 2>&1 |"; }
  else
    {
      $cmd = "cvs status -l $dir 2>&1 |";

      #  Note that we've checked this dir.
      #
      $Edir{$normdir} = 1;
    }
  
  if (! open(S, $cmd))
    {
      printf TTYO "$Mynamebase: open(\"$cmd\") filed: $!\n";
      return 1;
    }

  while (<S>)
    {
      chop;
      if (/^File: ([^\s]+)\s+Status: (.*)/)
        {
          $file = $1; $state = $2;

          #  Skip files we've already seen.
          #
          if (defined($Efile{"$normdir/$file"})) { next; }

	  #  Note that we've checked this file.
          #
          $Efile{"$normdir/$file"} = 1;

          if ($state =~ /^(Locally Modified|Locally Added)/)
            {
              #  Got one!
              #
              if (! defined($Dir{$normdir})) { &adddir($normdir); }
              if (defined($Files{$normdir})) { $Files{$normdir} .= " "; }
              $Files{$normdir} .= $file;
              $Cfiles{"$normdir/$file"} = 1;
            }
          if ($state =~ /^Needs Merge/)
            {
              print TTYO "$Mynamebase: $dir/$file needs merge.\n";
              $status = 1;
            }
          if ($state =~ /^Unresolved Conflict/)
            {
              print TTYO "$Mynamebase: $dir/$file has unresolved conflict.\n";
              $status = 1;
            }
        }
    }
  close S;

  return $status;
}


#  For this exercise, these directory names are pwd-normalized:
#
#  $Edir{<dir>}           is the list of directories we've checked with cvs status -l <dir>
#  $Efile{<path>}         is the list of files we've checked with cvs status <dir/file>
#  @Dir                   is the list of dirs wherein we have commits to do (in order detected)
#  $Files{<dir>}          is the list of files to commit in the dir (grouped by dir)
#  $Cfiles{<dir>/<file>}  is the list of files we're gonna commit (for quick lookup)
#  %Dir{<dir>}            is the list of dirs wherein we have commits to do (for quick lookup)

sub adddir
{
  my ($dir) = @_;  # must already be normalized!

  $Dir{$dir} = 1;
  push(@Dir, $dir);
  if (-f "$dir/$Planfile") { unlink "$dir/$Myname.plan"; }
}


sub norm
{
  my ($dir) = @_;

  #  Normalize the $dir name, just in case...
  #
  chdir $dir || die "couldn't chdir \"$dir\": $!";
  $dir = `/bin/pwd`; chop $dir;
  chdir $Here;
  return $dir;
}


sub checkfile
{
  my ($path) = @_;
  my $dir;
  my $normdir;
  my $file;
  my $cvstat;

  $dir = &dirname($path);
  ($file = $path) =~ s%^.*/%%;

  $normdir = &norm($dir);

  #  Don't check files we've already checked...
  #
  if (defined($Efile{"$normdir/$file"})) { return; } 

  #  ...or that we already have on the commit list...
  #
  if (defined($Cfiles{"$normdir/$file"})) { return; }

  #  OK, I guess we should run cvs status to check it.
  #
  return &cvstat($dir, $normdir, $file);
}


sub checkdir
{
  my ($dir) = @_;
  my $normdir;

  $normdir = &norm($dir);

  #  Don't examine directories we've already seen.
  #
  if (defined($Edir{$normdir})) { return; }
  
  #  Now run cvslines stat to find any mods...
  #
  return &cvstat($dir, $normdir);
}


sub traverse
{
  my ($dir, $lev) = @_;

  my $status = 0;
  
  my $dirent;
  my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
       $atime,$mtime,$ctime,$blksize,$blocks);

  my $dirhandle = "dh$lev";

  if (-d "$dir/CVS") { $status |= &checkdir($dir); }

  if ($Norecurse) { return $status; }

  opendir($dirhandle, $dir);

  while (($dirent = readdir($dirhandle)))
    {
      if ($dirent eq "." || $dirent eq "..") { next; }
      if (-d $dirent && $dirent eq "CVS") { next; }
      ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
       $atime,$mtime,$ctime,$blksize,$blocks) = lstat("$dir/$dirent");
      if (-d _) { $status |= traverse("$dir/$dirent", $lev+1); }
    }
  closedir($dirhandle);
  return $status;
}

#  We step aside if not a tty (or batch mode)...
#
if (! -t STDERR && ! $Testmode && ! $Batch)
  { print STDERR "$Myname: not a tty\n"; exit 0; }

if (! &openTTY()) { die "&openTTY() failed.\n"; }

$editor = "vi";
if (defined $ENV{"EDITOR"}) { $editor = $ENV{"EDITOR"}; }

#  Phase 0: check for existing $Planfile files, handle them
#

#  Construct the set of directories we're interested in
#

$status = 0;
$Merge_conflicts = "";

foreach $path (@Paths)
  {
    if    (-f $path) { $status |= &checkfile($path); }
    elsif (-d $path) { $status |= &traverse($path); }
    else  
      {
        $m = "\"$path\" is not a file or directory.";
	print TTYO "$Myname: $m\n"; &log($m);
        $status = 1;
      }
  }

if ($status) { &commit_cleanup(); }

if (! &module_config($Here))
  {
    print TTYO "$Myname: no $Mynamebase.config file for commit from \"$here\".\n";
    &commit_cleanup;
  }

$planning_ok = 1;

if ($Stickyans)
  {
    if (! open(STICKY, ">$Stickyans"))
      {
        print TTYO "$Myname: Can't create \"$Stickyans\": $!\n";
        &commit_cleanup;
      }
    close STICKY;
  }

#  Now we do the planning, by running cvslines_check for each
#  involved directory...
#
foreach $dir (@Dir)
  {
    my $cmd;

    $cmd = "$Mydirname/cvslines_check $dir $Files{$dir}";

    chdir $dir;
    $ENV{"CVSLINES_PHASE0"} = 1;
    if ($CVS_Remotehost ne "") { $ENV{"CVSLINES_REM"} = 1; }
    $status = system "$cmd";
    if ($status) { $planning_ok = 0; }
    delete $ENV{"CVSLINES_PHASE0"}; delete $ENV{"CVSLINES_REM"};
    chdir $Here;
  }

if (! $planning_ok)
  {
    $m = "there were problems in the planning phase.";
    print TTYO "$Myname: $m\n"; &log($m); &commit_cleanup();
  }

#  Phase II: execute the plan files...
#
#  Inhibit any attempts to run cvslines_check upon cvs commit.
#
$ENV{"CVSLINES_NOCHECK"} = "true";

foreach $dir (@Dir)
  { if (-f "$dir/$Planfile") { &commit_dir($dir); } }

unlink "$Logmsgfile";

&report_conflicts;

if ($Merge_conflicts ne "") { exit 1; }

exit 0;

