#!/usr/bin/perl

@temp = split(' ', '$Revision: 3.7 $ ');
$version = $temp[1];

use File::Copy;
use Getopt::Std;
use File::Path;

$homedir = $ENV{"HOME"} || $ENV{"LOGDIR"} || (getpwuid($<))[7];
if(substr($homedir,-1,1) ne "/") {$homedir = $homedir . "/";}

if($< == 0) {
        $configfile = "/etc/changetrack.conf";
        $historypath = "/var/lib/changetrack";
	}
else {
        $configfile =  $homedir . ".changetrackrc";
        $historypath = $homedir . ".changetrack/";
	}

$error = getopts('hc:d:a:m:erqM:vuo:');

if($opt_h || !$error || @ARGV)
	{
	if(@ARGV) {print "Unknown option: @ARGV\n";}
	print "
This is changetrack, version $version.
  This program keeps track of changes made to files.

  -h                          display this help and exit
  -c configfile               Set name of configuration file
  -d directory                Set output directory
  -e                          Make ed files for each one
  -r                          Don't use RCS
  -q                          Quiet mode -- display only important messages
  -m message                  Put a message in each file.
                              Useful for indicating reboots, etc.
                              Some special characters will break sh.
  -M message                  Like -m, except message is only printed for 
                              modified files.
  -v                          print version and exit.
  -u                          unified diffs. Tested with GNU diff
  -o emailaddress             Mail output to emailaddress. This is
                              supplemental to emails specified in the
                              config file.
";
	exit 1;  
	}

if ($opt_q) {
    $rcs_quiet = "-q";
} else {
    $rcs_quiet = "";
}
if($opt_v) {print "$version\n"; exit;}            # just the version
if($opt_c) {$configfile = $opt_c;}                # file storing files to check
if($opt_d) {$historypath = $opt_d;}               # directory to store output in
if($opt_u) {$diffargs = "-u";}                    # unified diffs

$message = $opt_m;                                # message (for reboots, etc.)
$Message = $opt_M;                                # other message.
if(substr($historypath,-1) ne "/") {$historypath = $historypath . "/";}
                                                  # needs to be a folder; forgot the '/'?

mkpath("$historypath",0,0711);                    # create it if it doesn't exist
mkpath("$historypath/RCS",0,0711);                # create RCS directory if it doesn't exist

$date = scalar localtime;                         # store the date in $date

open(CONFIG, "$configfile") or die "Exiting: can't open $configfile:$!\n";

# translate 'ls -l' to octal
sub getoctalbits {
    $out = "";
    $in = $_[0];
    $in = substr($in,1,9);
    @tmp = (substr($in,0,3),substr($in,3,3),substr($in,6,3));
    foreach $one (@tmp) {
	$num = 0;
	if(substr($one,0,1) ne "-") {$num += 4;}
	if(substr($one,1,1) ne "-") {$num += 2;}
	if(substr($one,2,1) ne "-") {$num += 1;}
	$out = $out . $num;
    }
    return $out;
}

if(!$opt_q) {print "Using $configfile, writing to $historypath\n";};
$emailaddresses = "";

while(<CONFIG>) {
    # for each config line
    chomp;
    
    #ignore comments
    if(m/\s*#/) {
       next; }
    
    # ignore blank lines
    if(m/^\s*$/) {
	next; }
       
    ($filename,$email,$options)=split(/\s+:\s+/); 
    # get the info
    @emails = split(/\s+/,$email);

    # add any address specified by -o on command line
    if (defined($opt_o) && ($opt_o ne '')) {
	push @emails, $opt_o;
    }

    # list of emails for this file
    foreach $email (@emails) {
	if(($x=index($emailaddresses,$email,0)) == -1) {
	    # if the user is not yet in the list, add them
	    $emailaddresses .= " " . $email;
	}
    }
    
    # make these relative to user's home directory
    if(substr($filename,0,1) ne "/") {
	$filename = $homedir . $filename;
    }
		
    open(LS,"ls -l -d $filename |");              # match wildcards like 'ls'
    $anyfile = 0;                                 # flag in case we find nothing
    while(<LS>) {                                 # may match several (wildcards)
	@diff = ();
	@ed = ();
	chomp;
	# the next line should be gone, and just a maximal match. How?
	while(m/  /) {s/  / /};
	@temp = split(/\s+/);
	$realfile = $temp[8];                     # filename
	if((substr($realfile,-1,1) eq "~") && ($filename =~ m/\*/)) {
	    # it's a backup file not explicitly included	
	    if(!$opt_q)
	    { print "Skipping backup file $realfile\n";}
	    next;	
	}
	
	$filebits = $temp[0];                     # permissions string
	if(substr($filebits,0,1) eq "d")
	{
	    if(!$opt_q)
	    { print "Skipping directory $realfile\n";}
	    
	    next;
	}
	$anyfile = 1;                             # at least one real file found
	$compfile = $realfile;                    # file for comparison
	
	@temp = stat $realfile;                   # other statistics:
	$fileuid = $temp[4];                      # owner,
	$filegid = $temp[5];                      # group
	
	while($compfile =~ m/\//) {               # replace '/' with ':'
	    $compfile =~ s/\//:/;}

	$compfile =~ s/^://;                      # trash leading ':'
	
	$compfile = $historypath . $compfile;
	$logfile = $compfile . ".history";        # stores past events
	$statfile = $compfile . ".statistics";    # stores current file info
	$origfile = $compfile . ".original";      # stores name of original file
	if($opt_e) {
	    $outfile = $compfile . ".edout";      # output from ed script
	    $edfile = $compfile . ".ed";          # ed script
	}
	$yestfile = $compfile . ".yesterday";     # stores current data
	
	if(!open(OLD,"$yestfile")) {              # can't open yesterday, doesn't exist.
	    @diff = (@diff, "New file $realfile\n");
	    if($opt_e) {
		@ed = (@ed,"# cat this file into ed, eg 'cat $edfile | ed'\n");
		@ed = (@ed,"# output goes into $outfile\n");
		@ed = (@ed,"# edit this file to get rid of commands you don't want.\n");
		@ed = (@ed,"\n!cp $origfile $outfile\n");
		@ed = (@ed,"E $outfile\n");
		copy($realfile, $origfile);       # keep a copy of original file
	    }

	    copy($realfile, $yestfile);           # so no changes noted today.
	    open (STAT, ">$statfile") or die "Exiting: can't open > $statfile:$!\n";
	    print STAT "$filebits\n$fileuid\n$filegid\n";
	    close(STAT);
	    if(!$opt_r) {
		`cp $realfile $compfile`;
		chdir($historypath);
		system("rcs $rcs_quiet -i -t-'this is $realfile' $compfile");
		`rcs $rcs_quiet -U $compfile`;
		`rm $compfile -f`;
	    }
	}
	else {
	    # only opening it to see if it exists.
	    close(OLD);
	}
	
	open(STAT, "$statfile") or die "Exiting: can't open < $statfile:$!\n";
	$oldfilebits = <STAT>;                    # get the comparison permissions
	chomp($oldfilebits);
	
	$oldfileuid = <STAT>;                     # get the old owner
	chomp($oldfileuid);

	$oldfilegid = <STAT>;                     # get the old group
	chomp($oldfilegid);

	close(STAT);

	$statschanged = 0;                        # 'nothing changed' flag

	$newnums = getoctalbits($filebits);
	if($oldfilebits ne $filebits) {
	    @diff = (@diff, "File permissions changed: was $oldfilebits now $filebits\n");
	    @ed = (@ed, "!chmod $newnums $outfile\n");
	    $statschanged = 1;
	}

	if($oldfileuid != $fileuid) {
	    $oldusername = getpwuid($oldfileuid);
	    $username = getpwuid($fileuid);
	    @diff = (@diff, "Owner changed: was $oldusername ($oldfileuid) now $username ($fileuid)\n");
	    @ed = (@ed,"!chown $fileuid $outfile\n");
	    $statschanged = 1;
	}

	if($oldfilegid != $filegid) {
	    $oldgroupname = getgrgid($oldfilegid);
	    $groupname = getgrgid($filegid);
	    @diff = (@diff, "Group changed: was $oldgroupname ($oldfilegid) now $groupname ($filegid)\n");
	    @ed = (@ed,"!chgrp $filegid $outfile\n");
	    $statschanged = 1;
	}

	if($statschanged) {
	    open(STAT, ">$statfile") or die "Exiting: can't open to rewrite $statfile:$!\n";
	    print STAT "$filebits\n$fileuid\n$filegid\n";
	    close(STAT);
	}

	open(DIFF, "diff $diffargs $yestfile $realfile |") or die "Exiting: can't run diff:$!\n";
	
	if(!$opt_q) {
	    print "$realfile";};
   	
	while(<DIFF>) {

	    # line starts with < or > or not unified header
	    if(m/^\</ || m/^\>/ || ($opt_u && !(m/^\-\-\-/||m/^\+\+\+/))) {
		if(!$opt_q) {
		    print ".";};                  # indicate progress
		
		@diff = (@diff, $_);              # get that line
	    }
	    $diff = 1;                            # flag the changes
     	}
	close(DIFF);
	
	if($diff) {
	    open(DIFF, "diff -e $yestfile $realfile |") or die "Can't do diff -e:$!\n";
	    # use -e to create ed commands
	    while(<DIFF>) {
		@ed = (@ed,"$_");                 # get the 'ed'-styled diffs. No need to understand them.
	    }
	    close(DIFF);
	}
	
	if(!$opt_q) {print "\n";};
	
	if(@diff || $message) {                   # there is something to add to the output file
	    # deal with emailing
	    foreach $email (@emails)
	    {
		# it's ok to append to things that don't exist.
		$emessages{$email} .= "Changes made to $realfile follow:\n";
		foreach $line (@diff) {
		    $emessages{$email} .= "  $line";
		}
		if($message) {
		    $emessages{$email} .= $message;}
		# don't forget the message
		$emessages{$email} .= "\n";       # separate from next file
	    }
	    
	    open(LOG,">>$logfile") or die "Exiting: can't open $logfile:$!\n";
	    print LOG "Changes made on $date follow:\n";
	    foreach $line (@diff)                     
	    {
		print LOG "  $line";              # save the line
	    }
	    if($message) {
		print LOG "  $message\n";         # save any message (nb after all changes)
	    }
	    if(@diff && $Message) {
		print LOG $Message;               # only if there are changes
	    }
	    print LOG "\n";                       # and a blank line
	    copy($realfile, $yestfile);           # save the file for next time
	    close(LOG);
	    
	    if($opt_e)
	    {
		open(ED,">>$edfile") or die "Exiting: can't open $edfile:$!\n";
		foreach $line (@ed) {
		    print ED $line;               # save the edits as well
		}	
		print ED "w\n";                   # make sure ed writes the changes when run.
		close(ED);
	    }
	    
	    if(!$opt_r) {
		chdir($historypath) or die "Can't chdir to $historypath for ci: $!\n";
		my $quiet = "";
		print "cp $realfile $compfile\n" unless defined($opt_q);
		`cp $realfile $compfile`;         # make backup copy
		#`mv $realfile $realfile.track`;  # copy backwards, to keep modification date
		#`cp $realfile.track $realfile`;  # make backup copy
		system("ci $rcs_quiet -m'modification of $realfile on $date' -l $compfile");
		`rm $compfile`;
	    }
	}
    }
    
    close(LS);
    if(!$anyfile) {
	# no file was matched by 'ls', so create message for misspelled files
	$origfile = $filename;
	while($filename =~ m/\//) {$filename =~ s/\//:/;}
	if(substr($filename,0,1) eq ":") {$filename = substr($filename,1);}
	open(LOG, ">>$historypath$filename");
	print LOG "$date No files match `$origfile'\n";
	close(LOG);
    }
}

# Exactly one of the next two lines should be commented out.
# If Mail::Sendmail (from CPAN) is installed, comment out the exit(),
# or else the program will terminate without sending emails.
# If Mail::Sendmail is not installed, comment out the 'use', or else
# this script will fail to run.
#
# Also, the $mailfrom variable must be a valid email address (or at least
# be from a valid domain).  Otherwise, outbound mail may get rejected by an
# intermediate MTA before it is delivered to your mailbox (the old
# 'changetrack@localhost' address is blocked by some anti-spam filters).

use Mail::Sendmail;
#exit();

my $mailfrom = 'changetrack@localhost';

if($emailaddresses) {

    @emails = split(/\s+/,$emailaddresses);
	
    foreach $email (@emails) {
	if(($email) && ($message = $emessages{$email})) {
	    %mail = (To => $email,
		     From => $mailfrom,
		     Message => "$message",
		     Subject => "changed files: $date"
		     );
	    
	    sendmail(%mail) or warn $Mail::Sendmail::error;
	}
    }
}

# $Log: changetrack,v $
# Revision 3.7  2001/11/16 02:08:16  cjmorlan
# Applied patch from Devin Reade
#
# Revision 3.6  2001/09/25 18:52:26  cjmorlan
# Applied patch from Devin Reade to fix -o option.
#
# Revision 3.5  2001/03/06 18:47:33  cjmorlan
# Intented according to emacs default.
# Fixed some @foo[]
#
# Revision 3.4  2001/03/06 18:09:55  cjmorlan
# Made version match RCS revision.
#
# Revision 3.3  2001/03/06 18:08:37  cjmorlan
# Added change from Ian Zimmerman, fixing RCS integration bug.
#
# Revision 3.2  1999/10/21 20:32:13  cjmorlan
# added email features, cleaned.
# Release version 2
#
# Revision 3.1  1999/10/20 18:04:54  cjmorlan
# replaced quotewords with split
#
# Revision 3.0  1999/09/24 04:45:03  cmorland
# To add ideas from FSF
#

