3 # cvsu - do a quick check to see what files are out of date.
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>
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 3 of the License, or
12 # (at your option) any later version.
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.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
29 use vars
qw($list_types %messages %options @batch_list $batch_cmd
30 $no_recurse $explain_type $find_mode $short_print
31 $no_cvsignore $nolinks $file $single_filename $curr_dir
32 @common_ignores $ignore_rx %entries %subdirs %removed);
34 use constant SUBDIR_FOUND => 1;
35 use constant SUBDIR_CVS => 2;
37 # This list comes from the CVS manual.
38 use constant STANDARD_IGNORES =>
39 ('RCS', 'SCCS', 'CVS', 'CVS.adm', 'RCSLOG', 'cvslog.*', 'tags',
40 'TAGS', '.make.state', '.nse_depinfo', '*~', '#*', '.#*', ',*',
41 "_\$*", "*\$", '*.old', '*.bak', '*.BAK', '*.orig', '*.rej',
42 '.del-*', '*.a', '*.olb', '*.o', '*.obj', '*.so', '*.exe',
43 '*.Z', '*.elc', '*.ln', 'core');
45 # 3-letter month names in POSIX locale, for fast date decoding
61 # print usage information and exit
65 " cvsu [OPTIONS] [FILE] ...\n" .
67 " --local Disable recursion\n" .
68 " --explain Verbosely print status of files\n" .
69 " --find Emulate find - filenames only\n" .
70 " --short Don't print paths\n" .
71 " --ignore Don't read .cvsignore\n" .
72 " --messages List known file types and long messages\n" .
73 " --nolinks Disable recognizing hard and soft links\n" .
74 " --types=[^]LIST Print only file types [not] from LIST\n" .
75 " --batch=COMMAND Execute this command on files\n" .
76 " --help Print this usage information\n" .
77 " --version Print version number\n" .
78 "Abbreviations and short options are supported\n";
82 # print version information and exit
85 print "cvsu - CVS offline examiner, version 0.2.3\n";
89 # If types begin with '^', make inversion
92 if ($list_types =~ m{^\^(.*)$}) {
94 foreach (keys %messages) {
96 if (index ($1, $_) < 0);
101 # list known messages and exit
105 print "Recognizable file types are:\n";
106 foreach (sort keys %messages) {
107 if (index($list_types, $_) >= 0) {
112 print " $default_mark $_ $messages{$_}\n";
114 print "* indicates file types listed by default\n";
118 # Initialize @common_ignores
119 # Also read $HOME/.cvsignore and append it to @common_ignores
122 my $HOME = $ENV{"HOME"};
124 push @common_ignores, STANDARD_IGNORES;
126 unless (defined($HOME)) {
130 my $home_cvsignore = "${HOME}/.cvsignore";
132 if (-f "$home_cvsignore") {
134 unless (open (CVSIGNORE, "< $home_cvsignore")) {
135 error ("couldn't open $home_cvsignore: $!");
138 while (<CVSIGNORE>) {
139 push (@common_ignores, split);
145 my $CVSIGNOREENV = $ENV{"CVSIGNORE"};
147 unless (defined($CVSIGNOREENV)) {
151 my @ignores_var = split (/ /, $CVSIGNOREENV);
152 push (@common_ignores, @ignores_var);
156 # Print message and exit (like "die", but without raising an exception).
157 # Newline is added at the end.
160 print STDERR "cvsu: ERROR: " . shift(@_) . "\n";
164 # execute commands from @exec_list with $exec_cmd
167 my @cmd_list = split (' ', $batch_cmd);
168 system (@cmd_list, @batch_list);
172 # Parameter 1: status in one-letter representation
175 my $type = shift (@_);
180 if $ignore_rx ne '' && $type =~ /[?SLD]/ && $file =~ /$ignore_rx/;
183 if (index($list_types, $type) < 0);
185 $pathfile = $curr_dir . $file;
187 if (defined($batch_cmd)) {
188 push (@batch_list, $pathfile);
189 # 1000 items in the command line might be too much for HP-UX
190 if ($#batch_list > 1000) {
205 $type = $messages{$type}
207 print "$type $item\n";
211 # load entries from CVS/Entries and CVS/Entries.Log
212 # Parameter 1: file name for CVS/Entries
213 # Return: list of entries in the format used in CVS/Entries
214 sub load_entries ($);
217 my $entries_file = shift (@_);
218 my $entries_log_file = "$entries_file.Log";
221 unless (open (ENTRIES, "< $entries_file")) {
222 error ("couldn't open $entries_file: $!");
230 if (open (ENTRIES, "< $entries_log_file")) {
235 } elsif ( m{^R (.+)} ) {
238 # Note: "cvs commit" helps even when you are offline
239 error ("$entries_log_file:$.: unrecognizable line, " .
240 "try \"cvs commit\"");
249 # process one directory
250 # Parameter 1: directory name
254 my $arg = shift (@_);
255 my %found_files = ();
257 # $file, $curr_dir, and $ignore_rx must be seen in file_status
259 local $ignore_rx = "";
260 local $single_filename = 0;
262 if ( $arg eq "" or -d $arg ) {
264 my $real_curr_dir = $curr_dir eq "" ? "." : $curr_dir;
266 error ("$real_curr_dir is not a directory")
267 unless ( -d $real_curr_dir );
269 # Scan present files.
271 opendir (DIR, $real_curr_dir) ||
272 error ("couldn't open directory $real_curr_dir: $!");
273 foreach (readdir (DIR)) {
274 $found_files {$_} = 1;
278 $single_filename = basename $arg;
279 $curr_dir = dirname $arg;
280 $found_files{$single_filename} = 1 if lstat $arg;
284 unless ( $curr_dir eq "" || $curr_dir =~ m{/$} );
291 foreach ( load_entries ("${curr_dir}CVS/Entries") ) {
292 if ( m{^D/([^/]+)/} ) {
293 $subdirs{$1} = SUBDIR_FOUND if !$single_filename;
294 } elsif ( m{^/([^/]+)/([^/])[^/]*/([^/]*)/} ) {
295 if ( !$single_filename or $single_filename eq $1 ) {
303 error ("${curr_dir}CVS/Entries: unrecognizable line");
307 if ( $single_filename && !$entries{$single_filename} &&
308 !$found_files{$single_filename} ) {
309 error ("nothing known about $arg");
312 # Scan .cvsignore if any
313 unless ($no_cvsignore) {
314 my (@ignore_list) = ();
316 if (-f "${curr_dir}.cvsignore") {
317 open (CVSIGNORE, "< ${curr_dir}.cvsignore")
318 || error ("couldn't open ${curr_dir}.cvsignore: $!");
319 while (<CVSIGNORE>) {
320 push (@ignore_list, split);
326 foreach $iter (@ignore_list, @common_ignores) {
330 if ($ignore_rx eq '') {
335 $ignore_rx .= glob_to_rx ($iter);
343 foreach $file (sort keys %entries) {
344 unless ($found_files{$file}) {
345 if ($removed{$file}) {
353 foreach $file (sort keys %found_files) {
354 next if ($file eq '.' || $file eq '..');
355 lstat ($curr_dir . $file) ||
356 error ("lstat() failed on $curr_dir . $file");
357 if (! $nolinks && -l _) {
360 if ($file eq 'CVS') {
362 } elsif ($subdirs{$file}) {
363 $subdirs{$file} = SUBDIR_CVS;
365 file_status ("D"); # Unknown directory
367 } elsif (! (-f _) && ! (-l _)) {
368 file_status ("S"); # This must be something very special
369 } elsif (! $nolinks && (stat _) [3] > 1 ) {
370 file_status ("H"); # Hard link
371 } elsif (! $entries{$file}) {
373 } elsif ($entries{$file} =~ /^Initial |^dummy /) {
375 } elsif ($entries{$file} =~ /^Result of merge/) {
377 } elsif ($entries{$file} !~
378 /^(...) (...) (..) (..):(..):(..) (....)$/) {
379 error ("Invalid timestamp for $curr_dir$file: $entries{$file}");
381 my $cvtime = timegm($6, $5, $4, $3, $months{$2}, $7 - 1900);
382 my $mtime = (stat _) [9];
383 if ($cvtime == $mtime) {
385 } elsif ($cvtime < $mtime) {
393 # Now do directories.
394 unless ($no_recurse) {
395 my $save_curr_dir = $curr_dir;
396 foreach $file (sort keys %subdirs) {
397 if ($subdirs{$file} == SUBDIR_FOUND) {
398 $curr_dir = $save_curr_dir;
400 } elsif ($subdirs{$file} == SUBDIR_CVS) {
401 process_arg ($save_curr_dir . $file)
407 # Turn a glob into a regexp without recognizing square brackets.
408 sub glob_to_rx_simple ($)
411 # Quote all non-word characters, convert ? to . and * to .*
412 $expr =~ s/(\W)/\\$1/g;
413 $expr =~ s/\\\*/.*/g;
418 # Turn a glob into a regexp
423 # Find parts in square brackets and copy them literally
424 # Text outside brackets is processed by glob_to_rx_simple()
425 while ($expr ne '') {
426 if ($expr =~ /^(.*?)(\[.*?\])(.*)/) {
428 $result .= glob_to_rx_simple ($1) . $2;
430 $result .= glob_to_rx_simple ($expr);
439 # types of files to be listed
440 $list_types = "^.FCL";
442 # long status messages
444 "?" => "Unlisted file",
445 "." => "Known directory",
446 "F" => "Up-to-date file",
447 "C" => "CVS admin directory",
448 "M" => "Modified file",
449 "S" => "Special file",
450 "D" => "Unlisted directory",
451 "L" => "Symbolic link",
454 "X" => "Lost directory",
455 "A" => "Newly added",
457 "G" => "Result of merge",
458 "R" => "Removed file"
461 undef @batch_list; # List of files for batch processing
462 undef $batch_cmd; # Command to be executed on files
463 $no_recurse = 0; # If this is set, do only local files
464 $explain_type = 0; # Verbosely print status of files
465 $find_mode = 0; # Don't print status at all
466 $short_print = 0; # Print only filenames without path
467 $no_cvsignore = 0; # Ignore .cvsignore
468 $nolinks = 0; # Do not test for soft- or hard-links
469 my $want_msg = 0; # List possible filetypes and exit
470 my $want_help = 0; # Print help and exit
471 my $want_ver = 0; # Print version and exit
474 "types=s" => \$list_types,
475 "batch=s" => \$batch_cmd,
476 "local" => \$no_recurse,
477 "explain" => \$explain_type,
478 "find" => \$find_mode,
479 "short" => \$short_print,
480 "ignore" => \$no_cvsignore,
481 "messages" => \$want_msg,
482 "nolinks" => \$nolinks,
483 "help" => \$want_help,
484 "version" => \$want_ver
487 GetOptions(%options);
491 list_messages() if $want_msg;
492 usage() if $want_help;
493 version() if $want_ver;
495 unless ($no_cvsignore) {
507 if ($#batch_list >= 0) {