#! /usr/bin/python -W all

# cvsu - do a quick check to see what files are out of date.
# Initially written by Tom Tromey <tromey@cygnus.com>
# Rewritten by Pavel Roskin <proski@gnu.org>

# $Id: cvsu.py,v 1.1 2003/01/30 23:51:17 proski Exp $

import sys, string


# types of files to be listed
list_types = "^.FCL"

# long status messages
messages = {
	"?" : "Unlisted file",
	"." : "Known directory",
	"F" : "Up-to-date file",
	"C" : "CVS admin directory",
	"M" : "Modified file",
	"S" : "Special file",
	"D" : "Unlisted directory",
	"L" : "Symbolic link",
	"H" : "Hard link",
	"U" : "Lost file",
	"X" : "Lost directory",
	"A" : "Newly added",
	"O" : "Older copy",
	"G" : "Result of merge",
	"R" : "Removed file"
}


def usage():
    """print usage information and exit"""
    print """Usage:
--local		Disable recursion
--explain		Verbosely print status of files
--find		Emulate find - filenames only
--short		Don't print paths
--ignore		Don't read .cvsignore
--messages		List known file types and long messages
--types=[^]LIST	Print only file types [not] from LIST
--batch=COMMAND	Execute this command on files
--help		Print this usage information
--version		Print version number
Abbreviations and short options are supported
"""
    sys.exit(0)


def version():
    """print version information and exit"""
    print "cvsu - CVS offline examiner, version -VERSION-"
    sys.exit(0)


# If types begin with '^', make inversion
def adjust_types(types):
    if (string.find(types, "^") == 0):
	new_types = ""
	for key in messages.keys():
	    if (string.find(types, key) < 0):
		new_types = new_types + key
	types = new_types
    return types


def list_messages():
    """list known messages and exit"""
    print "Recognizable file types are:"
    for key in messages.keys():
	if (string.find(list_types, key) >= 0):
	    default_mark = "*"
	else:
	    default_mark = " "
	print "  %s %s %s" % (default_mark, key, messages[key])
    print "* indicates file types listed by default"
    sys.exit(0)


list_types = adjust_types(list_types)
list_messages()

'''
def init_ignores():
    """Initialize @standard_ignores
       Also read $HOME/.cvsignore and append it to @standard_ignores"""
    $HOME = $ENV{"HOME"}
    # This list comes from the CVS manual.
    @standard_ignores = ('RCS', 'SCCS', 'CVS', 'CVS.adm', 'RCSLOG',
			 'cvslog.*', 'tags', 'TAGS', '.make.state',
			 '.nse_depinfo', '*~', '#*', '.#*', ',*',
			 "_\$*", "*\$", '*.old', '*.bak', '*.BAK',
			 '*.orig', '*.rej', '.del-*', '*.a', '*.olb',
			 '*.o', '*.obj', '*.so', '*.exe', '*.Z',
			 '*.elc', '*.ln', 'core')

    unless (defined($HOME)):
	return

    $home_cvsignore = "${HOME}/.cvsignore"

    if (-f "$home_cvsignore"):

	unless (open (CVSIGNORE, "< $home_cvsignore")):
	    error ("couldn't open $home_cvsignore: $!")

	while (<CVSIGNORE>):
	    push (@standard_ignores, split)

	close (CVSIGNORE)

    $CVSIGNOREENV = $ENV{"CVSIGNORE"}

    unless (defined($CVSIGNOREENV)):
	return

    @ignores_var = split (/ /, $CVSIGNOREENV)
    push (@standard_ignores, @ignores_var)


def error (error)
    """Print message and exit (like "die", but without raising an exception).
       Newline is added at the end."""
    print STDERR "cvsu: ERROR: " . error . "\n"
    sys.exit(1)


def do_batch ()
    """execute commands from @exec_list with $exec_cmd"""
    @cmd_list = split (' ', $batch_cmd)
    system (@cmd_list,  @batch_list)


# print files status
# Parameter 1: status in one-letter representation
def file_status(type):

    return
	if $ignore_rx ne '' && $type =~ /[?SLD]/ && $file =~ /$ignore_rx/

    return
	if (index($list_types, $type) < 0)

    $pathfile = $curr_dir . $file

    if (defined($batch_cmd)):
	push (@batch_list, $pathfile)
	# 1000 items in the command line might be too much for HP-UX
	if (len(batch_list) > 1000):
	    do_batch()
	    undef @batch_list

    if ($short_print):
	$item = $file
    else:
	$item = $pathfile

    if ($find_mode):
	print "$item\n"
    else:
	$type = $messages{$type}
	    if ($explain_type)
	print "$type $item\n"


def process_dir (dir)
    """process one directory
       Parameter 1: directory name"""

    # 3-letter month names in POSIX locale
    %months = (
	"Jan" : 0,
	"Feb" : 1,
	"Mar" : 2,
	"Apr" : 3,
	"May" : 4,
	"Jun" : 5,
	"Jul" : 6,
	"Aug" : 7,
	"Sep" : 8,
	"Oct" : 9,
	"Nov" : 10,
	"Dec" : 11
    )

    # $file, $curr_dir, and $ignore_rx must be seen in file_status
    $file = ""
    $curr_dir = dir
    $ignore_rx = ''

    $curr_dir .= "/"
	unless ( $curr_dir eq "" || $curr_dir =~ m{/$} )

    $real_curr_dir = $curr_dir eq "" ? "." : $curr_dir

    error ("$real_curr_dir is not a directory")
	unless ( -d $real_curr_dir )

    # Scan present files.
    file_status (".")
    %found_files = ()
    opendir (DIR, $real_curr_dir) ||
	error ("couldn't open directory $real_curr_dir: $!")
    foreach (readdir (DIR)):
	$found_files {$_} = 1
    closedir (DIR)

    # Scan CVS/Entries.
    %entries = ()
    %subdirs = ()
    %removed = ()
    open (ENTRIES, "< ${curr_dir}CVS/Entries")
	|| error ("couldn't open ${curr_dir}CVS/Entries: $!")
    while (<ENTRIES>):
	if ( m{^D/([^/]+)/} ):
	    $subdirs{$1} = 1
	elif ( m{^/([^/]+)/([^/])[^/]*/([^/]*)/} ):
	    $entries{$1} = $3
	    $removed{$1} = 1
		if $2 eq '-'
	else:
	    error ("${curr_dir}CVS/Entries:$.: unrecognizable line")
		unless m{D}
    close (ENTRIES)

    # CVS/Entries.Log lists actions to be done in CVS/Entries
    # Currently only adding and deleting directories is known to be safe
    if ( open (ENTRIES, "< ${curr_dir}CVS/Entries.Log") ):
	while (<ENTRIES>):
	    if ( m{^A D/([^/]+)/} ):
		$subdirs{$1} = 1
	    elif ( m{^R D/([^/]+)/} ):
		delete $subdirs{$1}
	    else:
		# Note: "cvs commit" helps even when you are offline
		error ("${curr_dir}CVS/Entries.Log:$.: unrecognizable line, " .
			"try \"cvs commit\"")
	close (ENTRIES)

    # It is intentional to list CVS before reading .cvsignore
    $file = "CVS"
    file_status ("C")

    # Scan .cvsignore if any
    unless ($no_cvsignore):
	(@ignore_list) = ()

	if (-f "${curr_dir}.cvsignore"):
	    open (CVSIGNORE, "< ${curr_dir}.cvsignore")
		|| error ("couldn't open ${curr_dir}.cvsignore: $!")
	    while (<CVSIGNORE>):
		push (@ignore_list, split)
	    close (CVSIGNORE)

	($iter)
	foreach $iter (@ignore_list, @standard_ignores):
	    if ($iter eq '!'):
		$ignore_rx = ''
	    else:
		if ($ignore_rx eq ''):
		    $ignore_rx = '^('
		else:
		    $ignore_rx .= '|'
		}
		$ignore_rx .= glob_to_rx ($iter)
	    }
	}
	$ignore_rx .= ')$'
	    if $ignore_rx ne ''
    }

    # File is missing
    foreach $file (sort keys %entries):
	unless ($found_files{$file}):
	    if ($removed{$file}):
		file_status("R")
	    else:
		file_status("U")

    foreach $file (sort keys %found_files):
	next if ($file eq 'CVS' || $file eq '.' || $file eq '..')
	lstat ($curr_dir . $file); # Don't use stat() and -X on other files
	if ($nolinks && -l _):
	    file_status ("L")
	elif (-d _):
	    if ($subdirs{$file}):
		$subdirs{$file} = 2
	    else:
		file_status ("D"); # Unknown directory
	elif (! (-f _)):
	    file_status ("S"); # This must be something very special
	elif (! $nolinks && (stat _) [3] > 1 ):
	    file_status ("H"); # Hard link
	elif (! $entries{$file}):
	    file_status ("?")
	elif ($entries{$file} =~ /^Initial |^dum/):
	    file_status ("A")
	elif ($entries{$file} =~ /^Result of merge/):
	    file_status ("G")
	elif ($entries{$file} !~
		/^(...) (...) (..) (..):(..):(..) (....)$/):
	    error ("Invalid timestamp for $curr_dir$file: $entries{$file}")
	else:
	    $cvtime = timegm($6, $5, $4, $3, $months{$2}, $7 - 1900)
	    $mtime = (stat _) [9]
	    if ($cvtime == $mtime):
		file_status ("F")
	    elif ($cvtime < $mtime):
		file_status ("M")
	    else:
		file_status ("O")

    # Now do directories.
    unless ($no_recurse):
	$save_curr_dir = $curr_dir
	foreach $file (sort keys %subdirs):
	    if ($subdirs{$file} == 1):
		$curr_dir = $save_curr_dir
		file_status ("X")
	    elif ($subdirs{$file} == 2):
		process_dir ($save_curr_dir . $file)


def glob_to_rx_simple(expr):
    """Turn a glob into a regexp without recognizing square brackets."""
    # Quote all non-word characters, convert ? to . and * to .*
    $expr =~ s/(\W)/\\$1/g
    $expr =~ s/\\\*/.*/g
    $expr =~ s/\\\?/./g
    return $expr


def glob_to_rx(expr):
    """Turn a glob into a regexp"""
    $result = ''
    # Find parts in square brackets and copy them literally
    # Text outside brackets is processed by glob_to_rx_simple()
    while ($expr ne ''):
	if ($expr =~ /^(.*?)(\[.*?\])(.*)/):
	    $expr = $3
	    $result .= glob_to_rx_simple ($1) . $2
	else:
	    $result .= glob_to_rx_simple ($expr)
	    last
    return $result


def Main():
    undef @batch_list;		# List of files for batch processing
    undef $batch_cmd;		# Command to be executed on files
    $no_recurse = 0;		# If this is set, do only local files
    $explain_type = 0;		# Verbosely print status of files
    $find_mode = 0;		# Don't print status at all
    $short_print = 0;		# Print only filenames without path
    $no_cvsignore = 0;		# Ignore .cvsignore
    $nolinks = 0;		# Do not test for soft- or hard-links
    $want_msg = 0;		# List possible filetypes and exit
    $want_help = 0;		# Print help and exit
    $want_ver = 0;		# Print version and exit

    options = (
	"types=s"  : \$list_types,
	"batch=s"  : \$batch_cmd,
	"local"	   : \$no_recurse,
	"explain"  : \$explain_type,
	"find"	   : \$find_mode,
	"short"	   : \$short_print,
	"ignore"   : \$no_cvsignore,
	"messages" : \$want_msg,
	"nolinks"  : \$nolinks,
	"help"     : \$want_help,
	"version"  : \$want_ver
    )

    GetOptions(%options)

    adjust_types()

    list_messages() if $want_msg
    usage() if $want_help
    version() if $want_ver

    unless ($no_cvsignore):
	init_ignores()

    if (len(ARGV) < 0):
	@ARGV = ("")

    foreach (@ARGV):
	process_dir ($_)

    if (len(batch_list) >= 0):
	    do_batch()


Main()
'''
