#! /usr/bin/perl -W
#
# Adam Lackorzynski <adam@os.inf.tu-dresden.de>
#
#
# Commit parts of a file. Select interactively.
#

use strict;

my $pager = "vim -R -";

# svn specific stuff
sub get_file_list_to_diff(@)
{
  my %list;
  foreach my $p (@_) {
    my @o = `svn st $p`;
    next if $?;
    foreach (@o) {
      if (/^([ADMC]).....\s(.+)/) {
        die "Remove conflict of $_ first" if $1 eq 'C';
	$list{$2} = 1;
      }
    }
  }

  sort keys %list;
}

sub is_wc()
{
  return 1 if -d '.svn';
  return 0;
}

sub get_unmod_file($)
{
  my $f = shift;
  my $c;

  open(SVNCAT, "svn cat $f |") || die "Cannot launch 'svn cat $f': $!";
  while (<SVNCAT>) {
    $c .= $_;
  }
  close SVNCAT;
  return $c;
}

sub create_clean_wc_to($@)
{
  my $to = shift;

  # this is the very simple method that doesn't really work if you're
  # committing a change in the root of the working copy, I'd really like to
  # have a possibility to clone a working copy (with a path)
  #system("cp -a . $to");
  #system("cd $to && svn revert -R .");

  # get base repo URL
  my $o = `svn info`;
  die "Cannot get repo URL for wc creation" if $o !~ /URL:\s+(.+)$/m;
  my $url = $1;
  print "URL: $url\n";

  system("svn co -N $url $to");

  foreach my $e (@_) {
    my @direlems = split(/\//, $e);
    # remove last elem: the filename
    pop(@direlems);
    my $part = '';
    for my $i (@direlems) {
      # we are sure here that all those paths are relative to current dir
      # and have no . or ..
      $part .= $i.'/';
      if (! -d "$to/$part") {
        # do not consider revs, we're going to commit anyway
	system("cd $to && svn up -N $part");
      }
    }
  }

}

sub call_interactive_diff_and_merge_tool($$)
{
  my $filewc = shift;
  my $filecommit = shift;

  system("vim -d $filewc $filecommit");
}

sub call_diff_of_wc($)
{
  my $d = shift;
  `cd $d && svn diff`;
}

sub call_interactive_diff_of_wc($)
{
  my $d = shift;
  system("cd $d && svn diff | $pager");
}

sub call_commit($)
{
  my $d = shift;
  system("cd $d && svn ci");
}

sub call_update(@)
{
  system("svn up ".join(' ', @_));
}


#########################################################################
## generic stuff

if (!defined($ARGV[0])) {
  print "use: $0 dir(s)/file(s)\n\n";
  print "     $0 .\n";
  print "     $0 foo.c blah.c\n";
  exit(1);
}

foreach (@ARGV) {
  die "No .. allowed (you may fix me)" if /^\.\.\// || /\/\.\.\// || /\/\.\.$/;
  die "No absolute path allowed (you may fix me)" if /^\//;
  die "$_ is no file or directory" unless -e;
}

die "Current directory is not a working copy." unless is_wc();

my @filelist = get_file_list_to_diff(@ARGV);

printf join("\n", @filelist)."\n";


# prepare

my $tmpdir = `mktemp -d`;
chomp($tmpdir);

my $filecommit = "$tmpdir/TO-COMMIT.c";
my $filewc     = "$tmpdir/FROM-WC.c";

my $unmodwc = "$tmpdir/unmod-wc";
mkdir $unmodwc;

print "Creating clean working copy of your current working copy:\n";
create_clean_wc_to($unmodwc, @filelist);
print "done.\n";

# build two files

open(TOCOMMIT, ">$filecommit") || die "Cannot open $filecommit: $!";
open(WC,       ">$filewc")     || die "Cannot open $filewc: $!";

foreach my $f (@filelist) {
  # repo version of the file
  print TOCOMMIT "========== PARTCOMMIT:$f ================\n";
  print TOCOMMIT get_unmod_file($f);

  # modified version of the file
  open(MODIFIED, $f) || die "Cannot open $f: $!";
  print WC "========== PARTCOMMIT:$f ================\n";
  while (<MODIFIED>) {
    print WC;
  }
  close MODIFIED;
}

close TOCOMMIT;
close WC;

# this one should not be modified
chmod 0400, $filewc;

call_interactive_diff_and_merge_tool($filewc, $filecommit);

# now unsplit the file and apply to the unmod working copy
open(TOCOMMIT, "$filecommit") || die "Cannot open $filecommit: $!";

sub _writeout($$)
{
  my $file = shift;
  my $content = shift;
  print "Writing $file\n";
  open(X, ">$file") || die "Cannot open $file: $!";
  print X $content;
  close X;
}

my $file;
my $filecontents = '';
while (<TOCOMMIT>) {
  if (/^========== PARTCOMMIT:(.+) ================$/) {
    # write out file to $unmodwc/$file
    _writeout("$unmodwc/$file", $filecontents) if defined $file;
    $file = $1;
    $filecontents = '';
  } else {
    die "Invalid file contents" if !defined($file);
    $filecontents .= $_;
  }
}
_writeout("$unmodwc/$file", $filecontents) if defined $file;

close TOCOMMIT;

if (call_diff_of_wc($unmodwc) ne '') {
  print "Check this:\n";
  call_interactive_diff_of_wc($unmodwc);

  print "Commit the diff you just saw? [y/N]\n";
  my $answer = `bash -c 'read a && echo \$a'`;
  chomp $answer;
  if (lc($answer) eq 'y') {
    call_commit($unmodwc);
    call_update(@filelist);
  } else {
    print "Not commiting stuff, bye\n";
  }
} else {
  print "No change, exiting\n";
}

system("rm -fr $tmpdir");
