Index: locker/sbin/build-update
===================================================================
--- locker/sbin/build-update	(revision 720)
+++ locker/sbin/build-update	(revision 720)
@@ -0,0 +1,144 @@
+#!/usr/bin/env perl
+
+use File::Spec::Functions qw(:ALL);
+use File::Copy;
+use File::Path;
+use Data::Dumper;
+use Getopt::Long;
+use Archive::Tar;
+use Cwd;
+use strict;
+use warnings;
+
+my $scriptsdev = "";
+
+GetOptions(
+#"redo-delete" => \$redodelete,
+#	   "redo-add" => \$redoadd,
+#	   "redo-replace" => \$redoreplace,
+#	   "redo-diff" => \$redodiff,
+#	   "redo-all" => sub {$redodelete = $redoadd = $redoreplace = $redodiff = 1;},
+	   "dev" => sub {$scriptsdev = "dev";},
+		  );
+
+if (@ARGV < 3) {
+  print STDERR "Usage: $0 package oldversion newversion\n";
+  exit(1);
+}
+
+my ($package, $oldversion, $newversion) = @ARGV;
+my ($old, $new, $updatename) = ($package.'-'.$oldversion, $package.'-'.$newversion, $package.'-'.$oldversion.'-to-'.$newversion);
+
+my $proposaldir = $updatename.".proposal";
+my $outdir = catdir("/mit/scripts/deploy$scriptsdev/updates/", $updatename);
+
+(-d $proposaldir) or die "Can't find $proposaldir, did you run propose-update?\n";
+((! -e $outdir) && mkdir($outdir)) or die "mkdir($outdir) failed: $!";
+
+my $olddir = catdir($proposaldir,$old);
+my $newdir = catdir($proposaldir,$new);
+(-d $olddir && -d $newdir) or die "Packages not unpacked?\n";
+
+sub readFileList($) {
+  my ($list) = @_;
+  open(LIST, $list) or die "Can't read $list: $!\n";
+  my @files = map { chomp; s|$newdir\/?||g; [split(' ', $_, 2)] } <LIST>;
+  close(LIST);
+  return @files;
+}
+
+my @todelete = readFileList(catfile($proposaldir, "files.delete"));
+my @toadd = readFileList(catfile($proposaldir, "files.add"));
+my @toreplace = readFileList(catfile($proposaldir, "files.replace"));
+
+open(CHECKMD5, ">", catfile($outdir, "check.md5")) or die "Can't open check.md5: $!";
+print CHECKMD5 map { join("  ", @{$_})."\n" } @todelete, @toreplace;
+close(CHECKMD5);
+
+open(ABSENT, ">", catfile($outdir, "oldfiles.absent")) or die "Can't open oldfiles.absent: $!";
+print ABSENT map { $_->[1]."\n" } @toadd;
+close(ABSENT);
+
+open(DELETE, ">", catfile($outdir, "files.delete")) or die "Can't open files.delete: $!";
+print DELETE map { $_->[1]."\n" } @todelete, @toreplace;
+close(DELETE);
+
+my $outfiletarball = catfile($outdir, "newfiles.tar.gz");
+my $cwd = getcwd();
+
+chdir($newdir);
+my $tar = Archive::Tar->new;
+$tar->add_files(map { $_->[1] } @toadd, @toreplace);
+$tar->write($outfiletarball, 1);
+chdir($cwd) or die "Couldn't chdir back to $cwd: $!\n";
+
+if (0) {
+    my $outfiledir = catdir($outdir, "newfiles");
+    (-d $outfiledir || mkdir($outfiledir)) or die "Can't mkdir($outfiledir)\n";
+    
+    foreach my $file (@toadd, @toreplace) {
+	my $filename = $file->[1];
+	my $src = catfile($newdir, $filename);
+	my $dest = catfile($outfiledir, $filename);
+	my (undef, $dir, undef) = splitpath($dest);
+	mkpath($dir);
+	copy($src, $dest) or die "Couldn't copy $filename from $src to $dest: $!";
+    }
+}
+
+copy(catfile($proposaldir, "update.diff"), catfile($outdir, "update.diff")) or die "Couldn't copy update.diff: $!";
+
+open (SCRIPT, ">", catfile($outdir, "update")) or die "Couldn't write update: $!";
+printf SCRIPT <<'EOF', catfile("/mit/scripts/deploy/", $old), catfile("/mit/scripts/deploy/", $new);
+#!/bin/bash
+set -e -o noclobber
+
+die () { echo "$1" >&2; exit 1; }
+
+patchdir=$(dirname "$0")
+
+>.scripts-security-upd-lock || die "error: Unable to obtain update lock."
+
+packages=$(tail -n 1 .scripts-version)
+
+echo "[$PWD] begin dry run"
+
+found=""
+newpackages=""
+for package in $packages; do
+    if [ "$package" = "%s" ]; then
+    found="yes"
+    newpackages="$newpackages%s "
+    else
+    newpackages="$newpackages$package "
+    fi
+done
+
+[ "$found" = "yes" ] || die "error: Correct version not found."
+xargs -n1 test ! -e <"$patchdir/oldfiles.absent" || die "error: Conflicting files exist."
+md5sum -c "$patchdir/check.md5" || die "error: MD5 check failed."
+patch -stN --dry-run --no-backup-if-mismatch -p2 <"$patchdir/update.diff" || die "error: Patch dry run failed."
+
+echo "[$PWD] dry run OK, applying update"
+
+mv .scripts-version .scripts-version.old
+patch -stN --no-backup-if-mismatch -p2 <"$patchdir/update.diff" || :
+xargs rm -f <"$patchdir/files.delete"
+tar -xzf "$patchdir/newfiles.tar.gz"
+(
+    cat .scripts-version.old
+    echo
+    date "+%%F %%T %%z"
+    echo "$USER@$(hostname)"
+    echo "$patchdir"
+    echo "$newpackages"
+) >.scripts-version
+rm -f .scripts-version.old
+
+rm -f .scripts-security-upd-lock
+
+echo "[$PWD] done"
+
+exit 0
+EOF
+chmod 0755, catfile($outdir, "update");
Index: locker/sbin/propose-update
===================================================================
--- locker/sbin/propose-update	(revision 720)
+++ locker/sbin/propose-update	(revision 720)
@@ -0,0 +1,173 @@
+#!/usr/athena/bin/perl
+
+use File::Spec::Functions;
+use Data::Dumper;
+use Getopt::Long;
+use Cwd;
+
+my ($redodelete, $redoadd, $redoreplace, $redodiff) = (0,0,0,0);
+
+my $scriptsdev = "";
+
+GetOptions("redo-delete" => \$redodelete,
+	   "redo-add" => \$redoadd,
+	   "redo-replace" => \$redoreplace,
+	   "redo-diff" => \$redodiff,
+	   "redo-all" => sub {$redodelete = $redoadd = $redoreplace = $redodiff = 1;},
+	   "dev" => sub {$scriptsdev = "dev";},
+		  );
+
+if (@ARGV < 3) {
+  print STDERR "Usage: $0 [--redo-{delete,add,replace,diff,all}] package oldversion newversion\n";
+  exit(1);
+}
+
+my ($package, $oldversion, $newversion) = @ARGV;
+my ($old, $new, $updatename) = ($package.'-'.$oldversion, $package.'-'.$newversion, $package.'-'.$oldversion.'-to-'.$newversion);
+
+my $outdir = $updatename.".proposal";
+
+(-d $outdir || mkdir($outdir)) or die "mkdir($outdir) failed: $!";
+
+my $olddir = catdir($outdir,$old);
+my $newdir = catdir($outdir,$new);
+
+unpackPackage($old, $olddir);
+unpackPackage($new, $newdir);
+
+sub unpackPackage($$) {
+  my ($package, $dir) = @_;
+  print STDERR "Extracting $package to $dir... ";
+  if (-d $dir) {
+    warn "$dir already exists; assuming unpacking was successful";
+    return;
+  }
+  mkdir($dir) or die "mkdir($dir) failed: $!";
+  my $cwd = cwd();
+  chdir($dir) or die $!;
+  `athrun scripts gtar zxf "/mit/scripts/deploy$scriptsdev/$package/$package.tar.gz"`;
+  if ($?) { chdir($cwd); system("rmdir", "$dir"); die "Failed to unpack $package.tar.gz: $?"; }
+  my @files=`athrun scripts gfind . -mindepth 1 -maxdepth 1 | grep -v .admin`;
+  if (@files <= 1) {
+    `athrun scripts gfind . -mindepth 2 -maxdepth 2 | xargs -i mv \{} .`;
+    rmdir($files[0]);
+  }
+  chdir($cwd) or die "Couldn't return to $cwd";
+  print "done.\n";
+}
+
+my @oldfiles = sort { $a->[1] cmp $b->[1] } map { chomp; s|$olddir\/?||g; [split(' ', $_, 2)] } `athrun scripts gfind $olddir -type f | xargs -i md5sum \{}`;
+#print Dumper(\@oldfiles);
+my @newfiles = sort { $a->[1] cmp $b->[1] } map { chomp; s|$newdir\/?||g; [split(' ', $_, 2)] } `athrun scripts gfind $newdir -type f | xargs -i md5sum \{}`;
+#print Dumper(\@newfiles);
+
+sub compareDirectories($$) {
+  my ($alist, $blist) = @_;
+  my @a = @$alist;
+  my @b = @$blist;
+  my @aonly, @bonly, @both;
+  $a = $b = 0;
+  my $debug = 0;
+  local $Data::Dumper::Indent = 0;
+  while ($a <= $#a || $b <= $#a) {
+    my $fa = $a[$a];
+    my $fb = $b[$b];
+    print STDERR "Comparing ".Dumper($fa, $fb)."\n" if $debug;
+    if ($fa->[1] eq $fb->[1]) { # Same file exists on both
+      print STDERR "Same file\n" if $debug;
+      if ($fa->[0] ne $fb->[0]) { # File has changed in some way
+	print STDERR "Different md5, pushing on \@both\n" if $debug;
+	push(@both, [$fa->[1], $fa, $fb]);
+      }
+      $a++; $b++; # increment both counters
+    } else {
+      my $a2 = $a;
+      while ($a2 <= $#a && $a[$a2]->[1] lt $fb->[1]) {
+	$a2++;
+      }
+      if ($a2 <= $#a && $a[$a2]->[1] eq $fb->[1]) {
+	for my $i ($a..$a2-1) {
+	  push @aonly, $a[$i];
+	}
+	$a = $a2;
+      } else {
+	my $b2 = $b;
+	while ($b2 <= $#b && $b[$b2]->[1] lt $fa->[1]) {
+	  $b2++;
+	}
+	if ($b2 <= $#b && $b[$b2]->[1] eq $fa->[1]) {
+	  for my $i ($b..$b2-1) {
+	    push @bonly, $b[$i];
+	  }
+	  $b = $b2;
+	} else {
+	  push @aonly, $a[$a];
+	  push @bonly, $b[$b];
+	  $a++; $b++;
+	}
+      }
+    }
+  }
+  return (\@aonly, \@bonly, \@both);
+}
+
+my (@todelete, @toadd, @changed);
+my @comp = compareDirectories(\@oldfiles, \@newfiles);
+open(DIFF, ">", catfile($outdir, "diff.pl"));
+print DIFF Dumper(@comp);
+close(DIFF);
+@todelete = @{$comp[0]};
+@toadd = @{$comp[1]};
+@changed = @{$comp[2]};
+
+if ($redodelete or ! -e catfile($outdir, "files.delete")) {
+	open(TODELETE, ">", catfile($outdir, "files.delete")) or die "Can't open files.delete: $!";
+	foreach my $file (@todelete) {
+	  printf TODELETE "%s %s\n", $file->[0], $file->[1];
+	}
+	close(TODELETE);
+	printf "Wrote %d filenames to files.delete\n", scalar(@todelete);
+} else { printf "Not overwriting existing files.delete\n"; }
+
+if ($redoadd or ! -e catfile($outdir, "files.add")) {
+	open(TOADD, ">", catfile($outdir, "files.add")) or die "Can't open files.add: $!";
+	foreach my $file (@toadd) {
+	  printf TOADD "%s %s\n", $file->[0], $file->[1];
+	}
+	close(TOADD);
+	printf "Wrote %d filenames to files.add\n", scalar(@toadd);
+} else { printf "Not overwriting existing files.add\n"; }
+
+my @toreplace;
+my @topatch;
+
+foreach my $file (@changed) {
+	if (-B catdir($newdir, $file->[0])) {
+		push (@toreplace, $file);
+	} else {
+		push (@topatch, $file);
+	}
+}
+
+if ($redoreplace or ! -e catfile($outdir, "files.replace")) {
+	open(TOREPLACE, ">", catfile($outdir, "files.replace")) or die "Can't open files.replace: $!";
+	foreach my $file (@toreplace) {
+		printf TOREPLACE "%s %s\n", $file->[1][0], $file->[0];
+	}
+	close(TOREPLACE);
+	printf "Wrote %d filenames to files.replace\n", scalar(@toreplace);
+} else { printf "Not overwriting existing files.replace\n"; }
+
+if ($redodiff or ! -e catfile($outdir, "update.diff")) {
+    open(DIFF, ">", catfile($outdir, "update.diff")) or die "Can't open update.diff: $!";
+    foreach my $file (@topatch) {
+	my $filename = $file->[0];
+	my $oldfile = catfile($olddir, $file->[1][1]);
+	my $newfile = catfile($newdir, $file->[2][1]);
+	my $cmd = "diff -urN $oldfile $newfile";
+	print DIFF "$cmd\n";
+	print DIFF `$cmd`;
+    }
+    close(DIFF);
+    printf "Wrote %d diffs to update.diff\n", scalar(@topatch);
+} else { printf "Not overwriting existing update.patch\n"; }
Index: locker/sbin/scripts-patch
===================================================================
--- locker/sbin/scripts-patch	(revision 720)
+++ locker/sbin/scripts-patch	(revision 720)
@@ -0,0 +1,37 @@
+#!/bin/bash
+set -e
+
+die () { echo "$1" >&2; exit 1; }
+
+path="$1"
+url=$(echo "$path" | perl -pe '/^\/mit\/([^\/]*)\/web_scripts\/(.*)$/; $_ = "http://scripts.mit.edu/~$1/$2\n";')
+[ "$url" != "http://scripts.mit.edu/~/" ] || die "Usage: scripts-patch /mit/<user>/web_scripts/<path>"
+
+versionfile=$path/.scripts-version
+[ -f "$versionfile" ] || die "$path was not created by the scripts installer."
+[ -r "$versionfile" ] || die "[$path] Could not read .scripts-version file."
+
+# The format of the .scripts-version file is such an awful mess.
+#
+# I feel like that needed to be said.
+
+version=$(tail -n +2 "$versionfile" | head -n1 | perl -pe '/File: .* -> \`(.*)\.tar\.(gz|bz2)'\''$/; $_ = $1')
+
+updatespath=/mit/andersk/Public/scripts/updates
+[ -d "$updatespath" ] || die "Could not find updates repository."
+[ -d "$updatespath/$version" ] || die "[$path] No update available for $version."
+echo "[$path] Applying update for $version:"
+
+pagebefore=`mktemp -t scripts-patch.XXXXXXXXXX`
+pageafter=`mktemp -t scripts-patch.XXXXXXXXXX`
+
+url=$(echo "$path" | perl -pe '/^\/mit\/([^\/]*)\/web_scripts\/(.*)$/; $_ = "http://scripts.mit.edu/~$1/$2\n";')
+wget -q "$url/" -O "$pagebefore"
+
+(cd "$path"; "$updatespath/$version/update") || die "[$path] *** FAILED UPDATE *** of $version"
+
+wget -q "$url/" -O "$pageafter"
+echo "[$path] Diff from before/after upgrade:"
+diff -U0 "$pagebefore" "$pageafter" || :
+
+rm -f "$pagebefore" "$pageafter"
