* Makefile.maint (alpha beta major): Fix syntax error.
[coreutils.git] / build-aux / cvsu
blob03e3d0686c6ea2e987227732c24db708f8635e0a
1 #! /usr/bin/perl -w
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 2, or (at your option)
12 # 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, write to the Free Software
21 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
22 # 02111-1307, USA.
25 require 5.004;
26 use Getopt::Long;
27 use File::Basename;
28 use Time::Local;
29 use strict;
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);
36 use constant SUBDIR_FOUND => 1;
37 use constant SUBDIR_CVS => 2;
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');
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
63 # print usage information and exit
64 sub usage ()
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;
84 # print version information and exit
85 sub version ()
87 print "cvsu - CVS offline examiner, version 0.2.3\n";
88 exit 0;
91 # If types begin with '^', make inversion
92 sub adjust_types ()
94 if ($list_types =~ m{^\^(.*)$}) {
95 $list_types = "";
96 foreach (keys %messages) {
97 $list_types .= $_
98 if (index ($1, $_) < 0);
103 # list known messages and exit
104 sub list_messages ()
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 = " ";
114 print " $default_mark $_ $messages{$_}\n";
116 print "* indicates file types listed by default\n";
117 exit 0;
120 # Initialize @common_ignores
121 # Also read $HOME/.cvsignore and append it to @common_ignores
122 sub init_ignores ()
124 my $HOME = $ENV{"HOME"};
126 push @common_ignores, STANDARD_IGNORES;
128 unless (defined($HOME)) {
129 return;
132 my $home_cvsignore = "${HOME}/.cvsignore";
134 if (-f "$home_cvsignore") {
136 unless (open (CVSIGNORE, "< $home_cvsignore")) {
137 error ("couldn't open $home_cvsignore: $!");
140 while (<CVSIGNORE>) {
141 push (@common_ignores, split);
144 close (CVSIGNORE);
147 my $CVSIGNOREENV = $ENV{"CVSIGNORE"};
149 unless (defined($CVSIGNOREENV)) {
150 return;
153 my @ignores_var = split (/ /, $CVSIGNOREENV);
154 push (@common_ignores, @ignores_var);
158 # Print message and exit (like "die", but without raising an exception).
159 # Newline is added at the end.
160 sub error ($)
162 print STDERR "cvsu: ERROR: " . shift(@_) . "\n";
163 exit 1;
166 # execute commands from @exec_list with $exec_cmd
167 sub do_batch ()
169 my @cmd_list = split (' ', $batch_cmd);
170 system (@cmd_list, @batch_list);
173 # print files status
174 # Parameter 1: status in one-letter representation
175 sub file_status ($)
177 my $type = shift (@_);
178 my $item;
179 my $pathfile;
181 return
182 if $ignore_rx ne '' && $type =~ /[?SLD]/ && $file =~ /$ignore_rx/;
184 return
185 if (index($list_types, $type) < 0);
187 $pathfile = $curr_dir . $file;
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;
198 if ($short_print) {
199 $item = $file;
200 } else {
201 $item = $pathfile;
204 if ($find_mode) {
205 print "$item\n";
206 } else {
207 $type = $messages{$type}
208 if ($explain_type);
209 print "$type $item\n";
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 ($)
219 my $entries_file = shift (@_);
220 my $entries_log_file = "$entries_file.Log";
221 my %ent = ();
223 unless (open (ENTRIES, "< $entries_file")) {
224 error ("couldn't open $entries_file: $!");
226 while (<ENTRIES>) {
227 chomp;
228 $ent{$_} = 1;
230 close (ENTRIES);
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\"");
245 close (ENTRIES);
248 return keys %ent;
251 # process one directory
252 # Parameter 1: directory name
253 sub process_arg ($);
254 sub process_arg ($)
256 my $arg = shift (@_);
257 my %found_files = ();
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;
264 if ( $arg eq "" or -d $arg ) {
265 $curr_dir = $arg;
266 my $real_curr_dir = $curr_dir eq "" ? "." : $curr_dir;
268 error ("$real_curr_dir is not a directory")
269 unless ( -d $real_curr_dir );
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;
278 closedir (DIR);
279 } else {
280 $single_filename = basename $arg;
281 $curr_dir = dirname $arg;
282 $found_files{$single_filename} = 1 if lstat $arg;
285 $curr_dir .= "/"
286 unless ( $curr_dir eq "" || $curr_dir =~ m{/$} );
288 # Scan CVS/Entries.
289 my %entries = ();
290 my %subdirs = ();
291 my %removed = ();
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 '-';
302 } elsif ( m{^D$} ) {
303 next;
304 } else {
305 error ("${curr_dir}CVS/Entries: unrecognizable line");
309 if ( $single_filename && !$entries{$single_filename} &&
310 !$found_files{$single_filename} ) {
311 error ("nothing known about $arg");
314 # Scan .cvsignore if any
315 unless ($no_cvsignore) {
316 my (@ignore_list) = ();
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);
324 close (CVSIGNORE);
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 .= '|';
337 $ignore_rx .= glob_to_rx ($iter);
340 $ignore_rx .= ')$'
341 if $ignore_rx ne '';
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");
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
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");
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)
409 # Turn a glob into a regexp without recognizing square brackets.
410 sub glob_to_rx_simple ($)
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;
420 # Turn a glob into a regexp
421 sub glob_to_rx ($)
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;
436 return $result;
439 sub Main ()
441 # types of files to be listed
442 $list_types = "^.FCL";
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"
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
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
489 GetOptions(%options);
491 adjust_types();
493 list_messages() if $want_msg;
494 usage() if $want_help;
495 version() if $want_ver;
497 unless ($no_cvsignore) {
498 init_ignores();
501 if ($#ARGV < 0) {
502 @ARGV = ("");
505 foreach (@ARGV) {
506 process_arg ($_);
509 if ($#batch_list >= 0) {
510 do_batch();
514 Main();