
1: #! /usr/bin/perl -w 2: 3: # cvsu - do a quick check to see what files are out of date. 4: # 5: # Copyright (C) 2000-2005 Pavel Roskin <proski@gnu.org> 6: # Initially written by Tom Tromey <tromey@cygnus.com> 7: # Completely rewritten by Pavel Roskin <proski@gnu.org> 8: # 9: # This program is free software; you can redistribute it and/or modify 10: # it under the terms of the GNU General Public License as published by 11: # the Free Software Foundation; either version 2, or (at your option) 12: # any later version. 13: # 14: # This program is distributed in the hope that it will be useful, 15: # but WITHOUT ANY WARRANTY; without even the implied warranty of 16: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17: # GNU General Public License for more details. 18: # 19: # You should have received a copy of the GNU General Public License 20: # along with this program; if not, write to the Free Software 21: # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 22: # 02111-1307, USA. 23: 24: 25: require 5.004; 26: use Getopt::Long; 27: use File::Basename; 28: use Time::Local; 29: use strict; 30: 31: use vars qw($list_types %messages %options @batch_list $batch_cmd 32: $no_recurse $explain_type $find_mode $short_print 33: $no_cvsignore $nolinks $file $single_filename $curr_dir 34: @common_ignores $ignore_rx %entries %subdirs %removed); 35: 36: use constant SUBDIR_FOUND => 1; 37: use constant SUBDIR_CVS => 2; 38: 39: # This list comes from the CVS manual. 40: use constant STANDARD_IGNORES => 41: ('RCS', 'SCCS', 'CVS', 'CVS.adm', 'RCSLOG', 'cvslog.*', 'tags', 42: 'TAGS', '.make.state', '.nse_depinfo', '*~', '#*', '.#*', ',*', 43: "_\$*", "*\$", '*.old', '*.bak', '*.BAK', '*.orig', '*.rej', 44: '.del-*', '*.a', '*.olb', '*.o', '*.obj', '*.so', '*.exe', 45: '*.Z', '*.elc', '*.ln', 'core'); 46: 47: # 3-letter month names in POSIX locale, for fast date decoding 48: my %months = ( 49: "Jan" => 0, 50: "Feb" => 1, 51: "Mar" => 2, 52: "Apr" => 3, 53: "May" => 4, 54: "Jun" => 5, 55: "Jul" => 6, 56: "Aug" => 7, 57: "Sep" => 8, 58: "Oct" => 9, 59: "Nov" => 10, 60: "Dec" => 11 61: ); 62: 63: # print usage information and exit 64: sub usage () 65: { 66: print "Usage:\n" . 67: " cvsu [OPTIONS] [FILE] ...\n" . 68: "Options:\n" . 69: " --local Disable recursion\n" . 70: " --explain Verbosely print status of files\n" . 71: " --find Emulate find - filenames only\n" . 72: " --short Don't print paths\n" . 73: " --ignore Don't read .cvsignore\n" . 74: " --messages List known file types and long messages\n" . 75: " --nolinks Disable recognizing hard and soft links\n" . 76: " --types=[^]LIST Print only file types [not] from LIST\n" . 77: " --batch=COMMAND Execute this command on files\n" . 78: " --help Print this usage information\n" . 79: " --version Print version number\n" . 80: "Abbreviations and short options are supported\n"; 81: exit 0; 82: } 83: 84: # print version information and exit 85: sub version () 86: { 87: print "cvsu - CVS offline examiner, version 0.2.3\n"; 88: exit 0; 89: } 90: 91: # If types begin with '^', make inversion 92: sub adjust_types () 93: { 94: if ($list_types =~ m{^\^(.*)$}) { 95: $list_types = ""; 96: foreach (keys %messages) { 97: $list_types .= $_ 98: if (index ($1, $_) < 0); 99: } 100: } 101: } 102: 103: # list known messages and exit 104: sub list_messages () 105: { 106: my $default_mark; 107: print "Recognizable file types are:\n"; 108: foreach (sort keys %messages) { 109: if (index($list_types, $_) >= 0) { 110: $default_mark = "*"; 111: } else { 112: $default_mark = " "; 113: } 114: print " $default_mark $_ $messages{$_}\n"; 115: } 116: print "* indicates file types listed by default\n"; 117: exit 0; 118: } 119: 120: # Initialize @common_ignores 121: # Also read $HOME/.cvsignore and append it to @common_ignores 122: sub init_ignores () 123: { 124: my $HOME = $ENV{"HOME"}; 125: 126: push @common_ignores, STANDARD_IGNORES; 127: 128: unless (defined($HOME)) { 129: return; 130: } 131: 132: my $home_cvsignore = "${HOME}/.cvsignore"; 133: 134: if (-f "$home_cvsignore") { 135: 136: unless (open (CVSIGNORE, "< $home_cvsignore")) { 137: error ("couldn't open $home_cvsignore: $!"); 138: } 139: 140: while (<CVSIGNORE>) { 141: push (@common_ignores, split); 142: } 143: 144: close (CVSIGNORE); 145: } 146: 147: my $CVSIGNOREENV = $ENV{"CVSIGNORE"}; 148: 149: unless (defined($CVSIGNOREENV)) { 150: return; 151: } 152: 153: my @ignores_var = split (/ /, $CVSIGNOREENV); 154: push (@common_ignores, @ignores_var); 155: 156: } 157: 158: # Print message and exit (like "die", but without raising an exception). 159: # Newline is added at the end. 160: sub error ($) 161: { 162: print STDERR "cvsu: ERROR: " . shift(@_) . "\n"; 163: exit 1; 164: } 165: 166: # execute commands from @exec_list with $exec_cmd 167: sub do_batch () 168: { 169: my @cmd_list = split (' ', $batch_cmd); 170: system (@cmd_list, @batch_list); 171: } 172: 173: # print files status 174: # Parameter 1: status in one-letter representation 175: sub file_status ($) 176: { 177: my $type = shift (@_); 178: my $item; 179: my $pathfile; 180: 181: return 182: if $ignore_rx ne '' && $type =~ /[?SLD]/ && $file =~ /$ignore_rx/; 183: 184: return 185: if (index($list_types, $type) < 0); 186: 187: $pathfile = $curr_dir . $file; 188: 189: if (defined($batch_cmd)) { 190: push (@batch_list, $pathfile); 191: # 1000 items in the command line might be too much for HP-UX 192: if ($#batch_list > 1000) { 193: do_batch(); 194: undef @batch_list; 195: } 196: } 197: 198: if ($short_print) { 199: $item = $file; 200: } else { 201: $item = $pathfile; 202: } 203: 204: if ($find_mode) { 205: print "$item\n"; 206: } else { 207: $type = $messages{$type} 208: if ($explain_type); 209: print "$type $item\n"; 210: } 211: } 212: 213: # load entries from CVS/Entries and CVS/Entries.Log 214: # Parameter 1: file name for CVS/Entries 215: # Return: list of entries in the format used in CVS/Entries 216: sub load_entries ($); 217: sub load_entries ($) 218: { 219: my $entries_file = shift (@_); 220: my $entries_log_file = "$entries_file.Log"; 221: my %ent = (); 222: 223: unless (open (ENTRIES, "< $entries_file")) { 224: error ("couldn't open $entries_file: $!"); 225: } 226: while (<ENTRIES>) { 227: chomp; 228: $ent{$_} = 1; 229: } 230: close (ENTRIES); 231: 232: if (open (ENTRIES, "< $entries_log_file")) { 233: while (<ENTRIES>) { 234: chomp; 235: if ( m{^A (.+)} ) { 236: $ent{$1} = 1; 237: } elsif ( m{^R (.+)} ) { 238: delete $ent{$1}; 239: } else { 240: # Note: "cvs commit" helps even when you are offline 241: error ("$entries_log_file:$.: unrecognizable line, " . 242: "try \"cvs commit\""); 243: } 244: } 245: close (ENTRIES); 246: } 247: 248: return keys %ent; 249: } 250: 251: # process one directory 252: # Parameter 1: directory name 253: sub process_arg ($); 254: sub process_arg ($) 255: { 256: my $arg = shift (@_); 257: my %found_files = (); 258: 259: # $file, $curr_dir, and $ignore_rx must be seen in file_status 260: local $file = ""; 261: local $ignore_rx = ""; 262: local $single_filename = 0; 263: 264: if ( $arg eq "" or -d $arg ) { 265: $curr_dir = $arg; 266: my $real_curr_dir = $curr_dir eq "" ? "." : $curr_dir; 267: 268: error ("$real_curr_dir is not a directory") 269: unless ( -d $real_curr_dir ); 270: 271: # Scan present files. 272: file_status ("."); 273: opendir (DIR, $real_curr_dir) || 274: error ("couldn't open directory $real_curr_dir: $!"); 275: foreach (readdir (DIR)) { 276: $found_files {$_} = 1; 277: } 278: closedir (DIR); 279: } else { 280: $single_filename = basename $arg; 281: $curr_dir = dirname $arg; 282: $found_files{$single_filename} = 1 if lstat $arg; 283: } 284: 285: $curr_dir .= "/" 286: unless ( $curr_dir eq "" || $curr_dir =~ m{/$} ); 287: 288: # Scan CVS/Entries. 289: my %entries = (); 290: my %subdirs = (); 291: my %removed = (); 292: 293: foreach ( load_entries ("${curr_dir}CVS/Entries") ) { 294: if ( m{^D/([^/]+)/} ) { 295: $subdirs{$1} = SUBDIR_FOUND if !$single_filename; 296: } elsif ( m{^/([^/]+)/([^/])[^/]*/([^/]*)/} ) { 297: if ( !$single_filename or $single_filename eq $1 ) { 298: $entries{$1} = $3; 299: $removed{$1} = 1 300: if $2 eq '-'; 301: } 302: } elsif ( m{^D$} ) { 303: next; 304: } else { 305: error ("${curr_dir}CVS/Entries: unrecognizable line"); 306: } 307: } 308: 309: if ( $single_filename && !$entries{$single_filename} && 310: !$found_files{$single_filename} ) { 311: error ("nothing known about $arg"); 312: } 313: 314: # Scan .cvsignore if any 315: unless ($no_cvsignore) { 316: my (@ignore_list) = (); 317: 318: if (-f "${curr_dir}.cvsignore") { 319: open (CVSIGNORE, "< ${curr_dir}.cvsignore") 320: || error ("couldn't open ${curr_dir}.cvsignore: $!"); 321: while (<CVSIGNORE>) { 322: push (@ignore_list, split); 323: } 324: close (CVSIGNORE); 325: } 326: 327: my ($iter); 328: foreach $iter (@ignore_list, @common_ignores) { 329: if ($iter eq '!') { 330: $ignore_rx = '' 331: } else { 332: if ($ignore_rx eq '') { 333: $ignore_rx = '^('; 334: } else { 335: $ignore_rx .= '|'; 336: } 337: $ignore_rx .= glob_to_rx ($iter); 338: } 339: } 340: $ignore_rx .= ')$' 341: if $ignore_rx ne ''; 342: } 343: 344: # File is missing 345: foreach $file (sort keys %entries) { 346: unless ($found_files{$file}) { 347: if ($removed{$file}) { 348: file_status("R"); 349: } else { 350: file_status("U"); 351: } 352: } 353: } 354: 355: foreach $file (sort keys %found_files) { 356: next if ($file eq '.' || $file eq '..'); 357: lstat ($curr_dir . $file) || 358: error ("lstat() failed on $curr_dir . $file"); 359: if (! $nolinks && -l _) { 360: file_status ("L"); 361: } elsif (-d _) { 362: if ($file eq 'CVS') { 363: file_status ("C"); 364: } elsif ($subdirs{$file}) { 365: $subdirs{$file} = SUBDIR_CVS; 366: } else { 367: file_status ("D"); # Unknown directory 368: } 369: } elsif (! (-f _) && ! (-l _)) { 370: file_status ("S"); # This must be something very special 371: } elsif (! $nolinks && (stat _) [3] > 1 ) { 372: file_status ("H"); # Hard link 373: } elsif (! $entries{$file}) { 374: file_status ("?"); 375: } elsif ($entries{$file} =~ /^Initial |^dummy /) { 376: file_status ("A"); 377: } elsif ($entries{$file} =~ /^Result of merge/) { 378: file_status ("G"); 379: } elsif ($entries{$file} !~ 380: /^(...) (...) (..) (..):(..):(..) (....)$/) { 381: error ("Invalid timestamp for $curr_dir$file: $entries{$file}"); 382: } else { 383: my $cvtime = timegm($6, $5, $4, $3, $months{$2}, $7 - 1900); 384: my $mtime = (stat _) [9]; 385: if ($cvtime == $mtime) { 386: file_status ("F"); 387: } elsif ($cvtime < $mtime) { 388: file_status ("M"); 389: } else { 390: file_status ("O"); 391: } 392: } 393: } 394: 395: # Now do directories. 396: unless ($no_recurse) { 397: my $save_curr_dir = $curr_dir; 398: foreach $file (sort keys %subdirs) { 399: if ($subdirs{$file} == SUBDIR_FOUND) { 400: $curr_dir = $save_curr_dir; 401: file_status ("X"); 402: } elsif ($subdirs{$file} == SUBDIR_CVS) { 403: process_arg ($save_curr_dir . $file) 404: } 405: } 406: } 407: } 408: 409: # Turn a glob into a regexp without recognizing square brackets. 410: sub glob_to_rx_simple ($) 411: { 412: my ($expr) = @_; 413: # Quote all non-word characters, convert ? to . and * to .* 414: $expr =~ s/(\W)/\\$1/g; 415: $expr =~ s/\\\*/.*/g; 416: $expr =~ s/\\\?/./g; 417: return $expr; 418: } 419: 420: # Turn a glob into a regexp 421: sub glob_to_rx ($) 422: { 423: my $result = ''; 424: my ($expr) = @_; 425: # Find parts in square brackets and copy them literally 426: # Text outside brackets is processed by glob_to_rx_simple() 427: while ($expr ne '') { 428: if ($expr =~ /^(.*?)(\[.*?\])(.*)/) { 429: $expr = $3; 430: $result .= glob_to_rx_simple ($1) . $2; 431: } else { 432: $result .= glob_to_rx_simple ($expr); 433: last; 434: } 435: } 436: return $result; 437: } 438: 439: sub Main () 440: { 441: # types of files to be listed 442: $list_types = "^.FCL"; 443: 444: # long status messages 445: %messages = ( 446: "?" => "Unlisted file", 447: "." => "Known directory", 448: "F" => "Up-to-date file", 449: "C" => "CVS admin directory", 450: "M" => "Modified file", 451: "S" => "Special file", 452: "D" => "Unlisted directory", 453: "L" => "Symbolic link", 454: "H" => "Hard link", 455: "U" => "Lost file", 456: "X" => "Lost directory", 457: "A" => "Newly added", 458: "O" => "Older copy", 459: "G" => "Result of merge", 460: "R" => "Removed file" 461: ); 462: 463: undef @batch_list; # List of files for batch processing 464: undef $batch_cmd; # Command to be executed on files 465: $no_recurse = 0; # If this is set, do only local files 466: $explain_type = 0; # Verbosely print status of files 467: $find_mode = 0; # Don't print status at all 468: $short_print = 0; # Print only filenames without path 469: $no_cvsignore = 0; # Ignore .cvsignore 470: $nolinks = 0; # Do not test for soft- or hard-links 471: my $want_msg = 0; # List possible filetypes and exit 472: my $want_help = 0; # Print help and exit 473: my $want_ver = 0; # Print version and exit 474: 475: my %options = ( 476: "types=s" => \$list_types, 477: "batch=s" => \$batch_cmd, 478: "local" => \$no_recurse, 479: "explain" => \$explain_type, 480: "find" => \$find_mode, 481: "short" => \$short_print, 482: "ignore" => \$no_cvsignore, 483: "messages" => \$want_msg, 484: "nolinks" => \$nolinks, 485: "help" => \$want_help, 486: "version" => \$want_ver 487: ); 488: 489: GetOptions(%options); 490: 491: adjust_types(); 492: 493: list_messages() if $want_msg; 494: usage() if $want_help; 495: version() if $want_ver; 496: 497: unless ($no_cvsignore) { 498: init_ignores(); 499: } 500: 501: if ($#ARGV < 0) { 502: @ARGV = (""); 503: } 504: 505: foreach (@ARGV) { 506: process_arg ($_); 507: } 508: 509: if ($#batch_list >= 0) { 510: do_batch(); 511: } 512: } 513: 514: Main();