#!/usr/bin/perl -w
# NAME: cvsversion.pl
# AIM: To examine an input directory, seeking CVS folders and files,
# and reporting when the last update was done.
# 10/12/2010 - Review - remove no warnings message...
# 2010/02/14 - add -v[n] to report more. N is size of time list stack,
# and added -xf <file>, and -xd <dir> to EXCLUDE these items (CaSe sensitive!)
# 10/11/2009 - Some further tidying, and move recursive directory to end of function
# Add prtw(), and show_warnings()
# 09/11/2009 - Added bytes-to-ks for $total_bytes, and get LOCAL directory sizes
# 31/10/2009 - But it reported the latest FILE, but want the latest CVS file
# 15/08/2007 - geoff mclane - geoffair.net/mperl
use strict;
use warnings;
use File::stat; # to get the file date
use File::Basename;
use Time::gmtime;
### prt( "$0 ... Hello, World ...\n" );
my $in_folder = 'C:/FGCVS/xmlrpc-c';
###my $in_folder = 'C:/FGCVS/Tidy';

my @fnd_files = ();
my @fnd_cvs = ();
my %cvs_names = ();
my @local_totals = ();

my ($fcnt, $ccnt, $msg);
my $mxlen = 0;
my $mxsiz = 0;
my $szlen = 0;
my $latest = 0;
my $largest = 0;
my $most_recent = 0;
my $recent_file = '';
my $cvs_latest = 0;
my $cvs_file = '';
my $total_files = 0;
my $total_bytes = 0;
my $total_dirs = 0;
my $total_all = 0;

# options
my $exclude_built = 1;
my $verbosity = 1;

my @excluded_ext = qw( .old .bak .obj .err .pdb .lst .pch .ilk 
    .ncb .plg .OPT .idb .aps .sbr .suo .bsc .manifest .user
    .res .zip .exe .lib .dll .exp
);
my @excluded_vc = qw( .dsw .dsp .sln .vcproj );

my @exclude_others = qw( temp*.* mt.dep Buildlog.htm );

my @user_exclude = ();
my @excluded_dirs = ();
my %exclude_shown = ();
my @warnings = ();

# debug
my $dbg1 = 0;	# show processing folders ...
my $dbg2 = 0;	# show CVS/SVN file entries
my $dbg3 = 0;	# show CVS/SVN file types

sub prt($) {
	my ($m) = shift;
	print $m;
}

sub mydie($) {
	my ($m) = shift;
	die $m;
}


sub prtw($) {
   my ($msg) = @_;
   prt($msg);
   $msg =~ s/\n$//;
   push(@warnings,$msg);
}

sub show_warnings() {
   if (@warnings) {
      prt( "Got ".scalar @warnings." WARNINGS...\n" );
      foreach my $msg (@warnings) {
         prt( "$msg\n" );
      }
   } else {
      # prt( "No warnings issued.\n" );
   }
}

sub pgm_exit($$) {
   my ($val,$msg) = @_;
   show_warnings();
   if (length($msg)) {
      $msg .= "\n" if ( !($msg =~ /\n$/) );
      prt($msg);
   }
   exit($val);
}

sub get_YYYYMMDD_hhmmss_UTC($) {
    my ($t) = shift;
    # sec, min, hour, mday, mon, year, wday, yday, and isdst.
    my $tm = gmtime($t);
    my $m = sprintf( "%04d/%02d/%02d %02d:%02d:%02d",
        $tm->year() + 1900, $tm->mon() + 1, $tm->mday(), $tm->hour(), $tm->min(), $tm->sec());
    return $m;
}

sub get_YYYYMMDD_UTC($) {
    my ($t) = shift;
    # sec, min, hour, mday, mon, year, wday, yday, and isdst.
    my $tm = gmtime($t);
    my $m = sprintf( "%04d/%02d/%02d",
        $tm->year() + 1900, $tm->mon() + 1, $tm->mday());
    return $m;
}

sub mycmp_ascend_n2 {
    # push(@fnd_files, [$ff, $sb->mtime, $sb->size, $lev] );
	return  1 if (${$a}[1] < ${$b}[1]);
	return -1 if (${$a}[1] > ${$b}[1]);
	return 0;
}

sub in_excluded_vc($) {
    my ($lcext) = shift;
    foreach my $ext (@excluded_vc) {
        if ($lcext eq $ext) {
            return 1;
        }
    }
    return 0;
}

sub in_excluded_ext($) {
    my ($lcext) = shift;
    foreach my $ext (@excluded_ext) {
        if ($lcext eq $ext) {
            return 1;
        }
    }
    return 0;
}

sub in_excluded_user($) {
    my ($tf) = shift;
    foreach my $fil (@user_exclude) {
        return 1 if ($tf eq $fil);
    }
    return 0;
}

sub in_excluded_dirs($) {
    my ($tf) = shift;
    foreach my $fil (@excluded_dirs) {
        return 1 if ($tf eq $fil);
    }
    return 0;
}

sub in_excluded($) {
    my ($lcext) = shift;
    return 1 if in_excluded_ext($lcext);
    return 1 if in_excluded_vc($lcext);
    return 0;
}

sub is_an_excluded_file($) {
    my ($ff) = shift;
    my ($nam,$dir,$ext) = fileparse($ff, qr/\.[^.]*/ );
    return 1 if in_excluded(lc($ext));
    my $name = $nam . $ext;
    return 1 if ($name =~ /^temp/i);
    return 1 if (lc($name) eq 'mt.dep');
    return 1 if (lc($name) eq 'buildlog.htm' );
    return 1 if in_excluded_user($name);
    return 1 if ($name =~ /^generate\.stamp/i);
    return 0;
}

sub dos_2_unix($) {
	my ($du) = shift;
	$du =~ s/\\/\//g;
	return $du;
}

##################################################
# My particular 'nice number'
sub get_nn($) { # perl nice number nicenum add commas
	my ($n) = shift;
	if (length($n) > 3) {
		my $mod = length($n) % 3;
		my $ret = (($mod > 0) ? substr( $n, 0, $mod ) : '');
		my $mx = int( length($n) / 3 );
		for (my $i = 0; $i < $mx; $i++ ) {
			if (($mod == 0) && ($i == 0)) {
				$ret .= substr( $n, ($mod+(3*$i)), 3 );
			} else {
				$ret .= ',' . substr( $n, ($mod+(3*$i)), 3 );
			}
		}
		return $ret;
	}
	return $n;
}



sub show_per_time_list() {
    if ($verbosity > 1) {
        # more output
        my (@per_time,$i,$ff,$tm,$sz,$sf,$nm,$dir,$dt,$csz,$min1,$min2,$len,$max,$cnt);
        my @arr = ();
        $max = scalar @fnd_files;
        prt("Display of the latest $verbosity files, of $max...\n");
        @per_time = sort mycmp_ascend_n2 @fnd_files;
        $min1 = 0;
        $min2 = 0;
        $cnt = 0;
        for ($i = 0; $i < $max; $i++) {
            $ff = $per_time[$i][0];
            next if ($exclude_built && is_an_excluded_file($ff));
            $cnt++;
            last if ($cnt > $verbosity);
            $tm = $per_time[$i][1];
            $sz = $per_time[$i][2];
            $sf = substr($ff, (length($in_folder) + 1));
            ($nm, $dir) = fileparse( $sf );
            # $dt = get_YYYYMMDD_UTC($tm);
            $csz = get_nn($sz);
            $len = length($nm);
            $min1 = $len if ($len > $min1);
            $len = length($csz);
            $min2 = $len if ($len > $min2);
            push(@arr, [$ff,$tm,$sz]);  # store entry
        }
        $max = scalar @arr;
        for ($i = 0; $i < $max; $i++) {
            $ff = $arr[$i][0];
            $tm = $arr[$i][1];
            $sz = $arr[$i][2];
            $sf = substr($ff, (length($in_folder) + 1));
            ($nm, $dir) = fileparse( $sf );
            $dt = get_YYYYMMDD_hhmmss_UTC($tm);
            $csz = get_nn($sz);
            $nm .= ' ' while (length($nm) < $min1);
            $csz = " $csz" while (length($csz) < $min2);
            prt("$dt: $nm $csz, in $dir\n");
        }
    }
}

sub b2ks2($) {
   my ($d) = @_;
	my $oss;
	my $kss;
	my $lg = 0;
	my $ks = ($d / 1024); #// get Ks
	my $div = 1;
   if( $ks < 1024 ) {
      $div = 1;
      $oss = "KB";
   } elsif ( $ks < (1024*1024) ) {
	  $div = 1024;
      $oss = "MB";
   } elsif ( $ks < (1024*1024*1024) ) {
      $div = 1024 * 1024;
      $oss = "GB";
   } else {
      $div = 1024 * 1204 * 1240;
      $oss = "TB";
   }
   $kss = $ks / $div;
   $kss += 0.05;
   $kss *= 10;
   $lg = int($kss);
   $kss = $lg / 10;
   $kss .= '.0' if (!($kss =~ /\./));
   ###return( ($lg / 10) . " " . $oss );
   return "$kss$oss";
}

#                       0     1           2
# push(@local_totals, [ $inf, $loc_tsize, $loc_tcnt ]);  # keep totals in THIS folder
sub show_per_folder() {
   my $max = scalar @local_totals;
   my ($i,$siz,$cnt,$msize,$mcnt,$mso,$msc,$dirs,$dirc);
   $msize = 0;
   $mcnt  = 0;
   $mso = 0;
   $msc = 0;
   for ($i = 0; $i < $max; $i++) {
      #$dir = $local_totals[$i][0];
      $siz = $local_totals[$i][1];
      $cnt = $local_totals[$i][2];
      if ($siz > $msize) {
         $msize = $siz;
         $mso = $i;
      }
      if ($cnt > $mcnt) {
         $mcnt = $cnt;
         $msc = $i;
      }
   }

   $dirs = $local_totals[$mso][0];
   $siz  = $local_totals[$mso][1];

   $dirc = $local_totals[$msc][0];
   $cnt  = $local_totals[$msc][2];

   if ($mso == $msc) {
      prt("$max searched, [$dirs] was largest $siz, and most $cnt. ($mso)\n");
   } else {
      prt("$max searched, largest $siz was [$dirs]($mso), most $cnt was [$dirc]($msc).\n");
   }
}

parse_args(@ARGV);

prt( "Processing $in_folder ...\n" ) if ($dbg1);
process_dir( $in_folder, 0 );
$fcnt = scalar @fnd_files;
$ccnt = scalar @fnd_cvs;
prt( "Found $fcnt files, $ccnt in CVS/SVN folders ...\n" );

my $alfildate = show_latest_file();

for (my $i = 0; $i < $ccnt; $i++) {
	my $ff = $fnd_cvs[$i][0];
	my $sz = $fnd_cvs[$i][2];
	my $sf = substr($ff, (length($in_folder) + 1));
	my $len = length($sf);
	if ($len > $mxlen) {
		$mxlen = $len;
	}
	if ($sz > $mxsiz) {
		$mxsiz = $sz;
	}
}
$msg = get_nn($mxsiz);
$szlen = length($msg) + 1;

for (my $i = 0; $i < $ccnt; $i++) {
	my $ff = $fnd_cvs[$i][0];
	my $tm = $fnd_cvs[$i][1];
	my $sz = $fnd_cvs[$i][2];
	my $sf = substr($ff, (length($in_folder) + 1));
	###my ($nm, $dir, $ext) = fileparse( $sf, qr/\.[^.]*/ );
	my ($nm, $dir) = fileparse( $sf );
	if (defined $cvs_names{$nm}) {
		$cvs_names{$nm}++;
	} else {
		$cvs_names{$nm} = 1;
	}
	if ($sz > $largest) {
		$largest = $sz;
	}
	my $nsz = get_nn($sz);
	while(length($nsz) < $szlen) {
		$nsz = ' ' . $nsz;
	}
	$msg = $sf;
	while(length($msg) < $mxlen) {
		$msg .= ' ';
	}
	if ($tm > $latest) {
		$latest = $tm;
	}
   if ($tm > $cvs_latest) {
      $cvs_latest = $tm;
      $cvs_file   = $ff;
   }
	prt( "$msg ". localtime($tm). " $nsz\n" ) if ($dbg2);
}

foreach my $key (keys %cvs_names) {
	my $val = $cvs_names{$key};
	prt( "$key $val\n" ) if ($dbg3);
}

my $nsz = get_nn($largest);
while(length($nsz) < $szlen) {
	$nsz = ' ' . $nsz;
}
$msg = "Latest (largest)";
##while(length($msg) < $mxlen) {
##	$msg .= ' ';
##}
##prt( "$msg ". localtime($latest). " $nsz\n" );
if ($latest) {
    prt( "$msg ". get_YYYYMMDD_hhmmss_UTC($latest) . " $nsz\n" );
    prt( "FILES: ".get_YYYYMMDD_UTC($latest) );
}  else {
    prt( "$msg NO LATEST CVS/SVN TIME $nsz\n" );
    prt( "NO CVS/SVN FILES" );
}
prt( ", FILE: $alfildate" );
if ($most_recent > 0) {
    prt( ", MOST RECENT: ".get_YYYYMMDD_UTC($most_recent). " $recent_file" );
}
prt("\n");
show_per_folder();
$msg = ($cvs_latest ? get_YYYYMMDD_UTC($cvs_latest) : '');
prt("DIR: [$in_folder] $msg, files $total_files, bytes ".get_nn($total_bytes)." (".b2ks2($total_bytes).")");
prt(", dirs $total_dirs(".($total_all - $total_files).")\n");
if ($cvs_latest) {
   prt( "CVS: latest: ".get_YYYYMMDD_UTC($cvs_latest)." ($cvs_file)\n" );
}

show_per_time_list();

pgm_exit(0,"");

##########################
##### subs

sub show_latest_file {
    my $max = scalar @fnd_files;
    my ($i, $ff, $tm, $sz, $fn);
    my ($nam, $dir, $ext, $name);
    my $lasttime = 0;
    my $lastfile = '';
    my $lastsize = 0;
    my $rmsg = '';
    for ($i = 0; $i < $max; $i++) {
        $ff = $fnd_files[$i][0];
        $tm = $fnd_files[$i][1];
        $sz = $fnd_files[$i][2];
        $fn = substr($ff, (length($in_folder) + 1));
        if ($tm > $most_recent) {
            $most_recent = $tm;
            $recent_file = $fn;
        }
        if ($exclude_built) {
            ($nam,$dir,$ext) = fileparse($ff, qr/\.[^.]*/ );
            next if in_excluded(lc($ext));
            $name = $nam . $ext;
            next if ($name =~ /^temp/i);
            next if (lc($name) eq 'mt.dep');
            next if (lc($name) eq 'buildlog.htm' );
        }
        if ($tm > $lasttime) {
            $lasttime = $tm;
            $lastfile = $fn;
            $lastsize = $sz;
        }
    }
    if ($lasttime) {
        $rmsg = get_YYYYMMDD_UTC($lasttime). " $lastfile";
        prt( "Latest file [$lastfile], [". get_YYYYMMDD_hhmmss_UTC($lasttime) ."], size ".get_nn($lastsize)."\n" );
    } else {
        $rmsg = "None found!";
        prt( "No latest file found!???\n" );
    }
    return $rmsg;
}


sub is_in_respository_folder($) {
   my ($f) = shift;
   if ($f =~ /(\/|\\)CVS(\/|\\)/i) {
      return 1;
   } elsif ($f =~ /(\/|\\)\.svn(\/|\\)/i) {
      return 1;
   } elsif ($f =~ /(\/|\\)\.git(\/|\\)/i) {
      return 1;
   }
   return 0;
}


sub process_dir {
    my ($inf, $lev) = @_;
	prt( "$lev: Processing $inf folder ...\n" ) if ($dbg1);
    my @dirs = ();
    my ($sb,$ff);
	if ( opendir( DIR, $inf ) ) {
        my @files = readdir(DIR);
		closedir DIR;
        my $loc_tsize = 0;
        my $loc_tcnt  = 0;
        my $loc_tdirs = 0;
		foreach my $fl (@files) {
            $total_all++;
			next if (($fl eq '.') || ($fl eq '..'));
			$ff = $inf . "/" . $fl;
			if (-d $ff) {
                $total_dirs++;
                $loc_tdirs++;
                # process_dir( $ff, ($lev + 1) ); # process this sub-directory
                if (in_excluded_dirs($fl)) {
                    if (! defined $exclude_shown{$fl}) {
                        prtw( "Excluding directory [$fl]\n");
                        $exclude_shown{$fl} = 1;
                    }
                } else {
                    #prt( "Including directory [$fl]\n");
                    push(@dirs,$ff);
                }
            } else {
				if( $sb = stat($ff) ) {
                   $total_files++;
                   $total_bytes += $sb->size;
                   # local stats
                   $loc_tsize += $sb->size;
                   $loc_tcnt++;
                   if ( is_in_respository_folder($ff) ) {
                      # file is in a CVS, .git or .svn folder
                      push(@fnd_cvs, [$ff, $sb->mtime, $sb->size, $lev] );
                   } else {
                      push(@fnd_files, [$ff, $sb->mtime, $sb->size, $lev] );
                   }
                } else {
                   prtw("WARNING: stat FAILED on [$ff]!\n");
                }
			}
		}
      push(@local_totals, [ $inf, $loc_tsize, $loc_tcnt, $loc_tdirs ]);  # keep totals in THIS folder
      # now process any DIRECTORIES found, NOT in @excluded_dirs...
      foreach $ff (@dirs) {
         process_dir( $ff, ($lev + 1) ); # process this sub-directory
      }
	} else {
		prtw( "WARNING: Can NOT open $inf ... $! ...\n" );
	}
}

sub give_help {
	prt( "$0 [OPTIONS] in_folder\n" );
	prt( "OPTIONS:\n" );
	prt( " -? or -h - This brief help.\n" );
	prt( " -d1      - show processing folders.\n" );
	prt( " -d2      - show CVS/SVN file entries.\n" );
	prt( " -d3      - show CVS/SVN file types.\n" );
    prt( " -v[N}    - Increase verbosity. That is depth of times shown. Default is 1 = just latest\n");
    prt( " -xf file - Exclude this file name.\n");
    prt( " -xd dir  - Exclude this directory from search");
	mydie("In folder must exist ...\n");
}

sub check_nxt {
    my ($a,@v) = @_;
    mydir("ERROR: Argument [$a] MUST be followed by another argment to give the value! Aborting...")
        if (!@v);
}

sub parse_args { # @ARGV
	my (@av) = @_;
    my ($dn, @tv, $vb);
	while (@av) {
		my $arg = $av[0];
		if (substr($arg,0,1) eq '-') {
			if (($arg eq '-?')||($arg eq '-h')) {
				give_help();
			} elsif ($arg eq '-d1') {
				$dbg1 = 1;
				prt( "Show folder processing ...\n" );
			} elsif ($arg eq '-d2') {
				$dbg2 = 1;
				prt( "Show CVS/SVN file entries ...\n" );
			} elsif ($arg eq '-d3') {
				$dbg3 = 1;
				prt( "Show CVS/SVN file types ...\n" );
            } elsif ($arg =~ /^-v(.*)/) {
                # verbosity
                $vb = $1;
                if (defined $vb && length($vb)) {
                    if ($vb =~ /^\d+$/) {
                        $verbosity = $vb;
                    } else {
        				mydie( "ERROR: Unknown option [$arg] -v can only be followed by a number... aborting ...\n" );
                    }
                } else {
                    $verbosity++;
                }
                prt("Verbosity set to [$verbosity], per [$arg]\n");
            } elsif ($arg =~ /^-d(\d+)/) {
                $dn = $1;
                @tv = ();
                push(@tv,'-d3') if ($dn >= 3);
                push(@tv,'-d2') if ($dn >= 2);
                push(@tv,'-d1') if ($dn >= 1);
                parse_args(@tv) if (@tv);
            } elsif ($arg eq '-xf') {
                check_nxt(@av);
                shift @av;
                $arg = $av[0];
                push(@user_exclude,$arg);
                prt("Added [$arg] to user file excludes.\n");
            } elsif ($arg eq '-xd') {
                check_nxt(@av);
                shift @av;
                $arg = $av[0];
                push(@excluded_dirs,$arg);
                prt("Added [$arg] to excluded directories.\n");
			} else {
				mydie( "ERROR: Unknown option [$arg] ... aborting ...\n" );
			}
		} else {
			# bare item - assume INPUT folder
			$in_folder = dos_2_unix($arg);
			prt( "Set in folder to $in_folder ...\n" );
		}

		shift @av;
	}
}

# eof - cvsversion.pl
