mirror of
https://github.com/coreutils/coreutils.git
synced 2024-12-01 05:53:32 +08:00
515 lines
12 KiB
Perl
Executable File
515 lines
12 KiB
Perl
Executable File
#! /usr/bin/perl -w
|
|
|
|
# cvsu - do a quick check to see what files are out of date.
|
|
#
|
|
# Copyright (C) 2000-2005 Pavel Roskin <proski@gnu.org>
|
|
# Initially written by Tom Tromey <tromey@cygnus.com>
|
|
# Completely rewritten by Pavel Roskin <proski@gnu.org>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2, or (at your option)
|
|
# any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
|
|
# 02111-1307, USA.
|
|
|
|
|
|
require 5.004;
|
|
use Getopt::Long;
|
|
use File::Basename;
|
|
use Time::Local;
|
|
use strict;
|
|
|
|
use vars qw($list_types %messages %options @batch_list $batch_cmd
|
|
$no_recurse $explain_type $find_mode $short_print
|
|
$no_cvsignore $nolinks $file $single_filename $curr_dir
|
|
@common_ignores $ignore_rx %entries %subdirs %removed);
|
|
|
|
use constant SUBDIR_FOUND => 1;
|
|
use constant SUBDIR_CVS => 2;
|
|
|
|
# This list comes from the CVS manual.
|
|
use constant 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');
|
|
|
|
# 3-letter month names in POSIX locale, for fast date decoding
|
|
my %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
|
|
);
|
|
|
|
# print usage information and exit
|
|
sub usage ()
|
|
{
|
|
print "Usage:\n" .
|
|
" cvsu [OPTIONS] [FILE] ...\n" .
|
|
"Options:\n" .
|
|
" --local Disable recursion\n" .
|
|
" --explain Verbosely print status of files\n" .
|
|
" --find Emulate find - filenames only\n" .
|
|
" --short Don't print paths\n" .
|
|
" --ignore Don't read .cvsignore\n" .
|
|
" --messages List known file types and long messages\n" .
|
|
" --nolinks Disable recognizing hard and soft links\n" .
|
|
" --types=[^]LIST Print only file types [not] from LIST\n" .
|
|
" --batch=COMMAND Execute this command on files\n" .
|
|
" --help Print this usage information\n" .
|
|
" --version Print version number\n" .
|
|
"Abbreviations and short options are supported\n";
|
|
exit 0;
|
|
}
|
|
|
|
# print version information and exit
|
|
sub version ()
|
|
{
|
|
print "cvsu - CVS offline examiner, version 0.2.3\n";
|
|
exit 0;
|
|
}
|
|
|
|
# If types begin with '^', make inversion
|
|
sub adjust_types ()
|
|
{
|
|
if ($list_types =~ m{^\^(.*)$}) {
|
|
$list_types = "";
|
|
foreach (keys %messages) {
|
|
$list_types .= $_
|
|
if (index ($1, $_) < 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
# list known messages and exit
|
|
sub list_messages ()
|
|
{
|
|
my $default_mark;
|
|
print "Recognizable file types are:\n";
|
|
foreach (sort keys %messages) {
|
|
if (index($list_types, $_) >= 0) {
|
|
$default_mark = "*";
|
|
} else {
|
|
$default_mark = " ";
|
|
}
|
|
print " $default_mark $_ $messages{$_}\n";
|
|
}
|
|
print "* indicates file types listed by default\n";
|
|
exit 0;
|
|
}
|
|
|
|
# Initialize @common_ignores
|
|
# Also read $HOME/.cvsignore and append it to @common_ignores
|
|
sub init_ignores ()
|
|
{
|
|
my $HOME = $ENV{"HOME"};
|
|
|
|
push @common_ignores, STANDARD_IGNORES;
|
|
|
|
unless (defined($HOME)) {
|
|
return;
|
|
}
|
|
|
|
my $home_cvsignore = "${HOME}/.cvsignore";
|
|
|
|
if (-f "$home_cvsignore") {
|
|
|
|
unless (open (CVSIGNORE, "< $home_cvsignore")) {
|
|
error ("couldn't open $home_cvsignore: $!");
|
|
}
|
|
|
|
while (<CVSIGNORE>) {
|
|
push (@common_ignores, split);
|
|
}
|
|
|
|
close (CVSIGNORE);
|
|
}
|
|
|
|
my $CVSIGNOREENV = $ENV{"CVSIGNORE"};
|
|
|
|
unless (defined($CVSIGNOREENV)) {
|
|
return;
|
|
}
|
|
|
|
my @ignores_var = split (/ /, $CVSIGNOREENV);
|
|
push (@common_ignores, @ignores_var);
|
|
|
|
}
|
|
|
|
# Print message and exit (like "die", but without raising an exception).
|
|
# Newline is added at the end.
|
|
sub error ($)
|
|
{
|
|
print STDERR "cvsu: ERROR: " . shift(@_) . "\n";
|
|
exit 1;
|
|
}
|
|
|
|
# execute commands from @exec_list with $exec_cmd
|
|
sub do_batch ()
|
|
{
|
|
my @cmd_list = split (' ', $batch_cmd);
|
|
system (@cmd_list, @batch_list);
|
|
}
|
|
|
|
# print files status
|
|
# Parameter 1: status in one-letter representation
|
|
sub file_status ($)
|
|
{
|
|
my $type = shift (@_);
|
|
my $item;
|
|
my $pathfile;
|
|
|
|
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 ($#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";
|
|
}
|
|
}
|
|
|
|
# load entries from CVS/Entries and CVS/Entries.Log
|
|
# Parameter 1: file name for CVS/Entries
|
|
# Return: list of entries in the format used in CVS/Entries
|
|
sub load_entries ($);
|
|
sub load_entries ($)
|
|
{
|
|
my $entries_file = shift (@_);
|
|
my $entries_log_file = "$entries_file.Log";
|
|
my %ent = ();
|
|
|
|
unless (open (ENTRIES, "< $entries_file")) {
|
|
error ("couldn't open $entries_file: $!");
|
|
}
|
|
while (<ENTRIES>) {
|
|
chomp;
|
|
$ent{$_} = 1;
|
|
}
|
|
close (ENTRIES);
|
|
|
|
if (open (ENTRIES, "< $entries_log_file")) {
|
|
while (<ENTRIES>) {
|
|
chomp;
|
|
if ( m{^A (.+)} ) {
|
|
$ent{$1} = 1;
|
|
} elsif ( m{^R (.+)} ) {
|
|
delete $ent{$1};
|
|
} else {
|
|
# Note: "cvs commit" helps even when you are offline
|
|
error ("$entries_log_file:$.: unrecognizable line, " .
|
|
"try \"cvs commit\"");
|
|
}
|
|
}
|
|
close (ENTRIES);
|
|
}
|
|
|
|
return keys %ent;
|
|
}
|
|
|
|
# process one directory
|
|
# Parameter 1: directory name
|
|
sub process_arg ($);
|
|
sub process_arg ($)
|
|
{
|
|
my $arg = shift (@_);
|
|
my %found_files = ();
|
|
|
|
# $file, $curr_dir, and $ignore_rx must be seen in file_status
|
|
local $file = "";
|
|
local $ignore_rx = "";
|
|
local $single_filename = 0;
|
|
|
|
if ( $arg eq "" or -d $arg ) {
|
|
$curr_dir = $arg;
|
|
my $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 (".");
|
|
opendir (DIR, $real_curr_dir) ||
|
|
error ("couldn't open directory $real_curr_dir: $!");
|
|
foreach (readdir (DIR)) {
|
|
$found_files {$_} = 1;
|
|
}
|
|
closedir (DIR);
|
|
} else {
|
|
$single_filename = basename $arg;
|
|
$curr_dir = dirname $arg;
|
|
$found_files{$single_filename} = 1 if lstat $arg;
|
|
}
|
|
|
|
$curr_dir .= "/"
|
|
unless ( $curr_dir eq "" || $curr_dir =~ m{/$} );
|
|
|
|
# Scan CVS/Entries.
|
|
my %entries = ();
|
|
my %subdirs = ();
|
|
my %removed = ();
|
|
|
|
foreach ( load_entries ("${curr_dir}CVS/Entries") ) {
|
|
if ( m{^D/([^/]+)/} ) {
|
|
$subdirs{$1} = SUBDIR_FOUND if !$single_filename;
|
|
} elsif ( m{^/([^/]+)/([^/])[^/]*/([^/]*)/} ) {
|
|
if ( !$single_filename or $single_filename eq $1 ) {
|
|
$entries{$1} = $3;
|
|
$removed{$1} = 1
|
|
if $2 eq '-';
|
|
}
|
|
} elsif ( m{^D$} ) {
|
|
next;
|
|
} else {
|
|
error ("${curr_dir}CVS/Entries: unrecognizable line");
|
|
}
|
|
}
|
|
|
|
if ( $single_filename && !$entries{$single_filename} &&
|
|
!$found_files{$single_filename} ) {
|
|
error ("nothing known about $arg");
|
|
}
|
|
|
|
# Scan .cvsignore if any
|
|
unless ($no_cvsignore) {
|
|
my (@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);
|
|
}
|
|
|
|
my ($iter);
|
|
foreach $iter (@ignore_list, @common_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 '.' || $file eq '..');
|
|
lstat ($curr_dir . $file) ||
|
|
error ("lstat() failed on $curr_dir . $file");
|
|
if (! $nolinks && -l _) {
|
|
file_status ("L");
|
|
} elsif (-d _) {
|
|
if ($file eq 'CVS') {
|
|
file_status ("C");
|
|
} elsif ($subdirs{$file}) {
|
|
$subdirs{$file} = SUBDIR_CVS;
|
|
} else {
|
|
file_status ("D"); # Unknown directory
|
|
}
|
|
} elsif (! (-f _) && ! (-l _)) {
|
|
file_status ("S"); # This must be something very special
|
|
} elsif (! $nolinks && (stat _) [3] > 1 ) {
|
|
file_status ("H"); # Hard link
|
|
} elsif (! $entries{$file}) {
|
|
file_status ("?");
|
|
} elsif ($entries{$file} =~ /^Initial |^dummy /) {
|
|
file_status ("A");
|
|
} elsif ($entries{$file} =~ /^Result of merge/) {
|
|
file_status ("G");
|
|
} elsif ($entries{$file} !~
|
|
/^(...) (...) (..) (..):(..):(..) (....)$/) {
|
|
error ("Invalid timestamp for $curr_dir$file: $entries{$file}");
|
|
} else {
|
|
my $cvtime = timegm($6, $5, $4, $3, $months{$2}, $7 - 1900);
|
|
my $mtime = (stat _) [9];
|
|
if ($cvtime == $mtime) {
|
|
file_status ("F");
|
|
} elsif ($cvtime < $mtime) {
|
|
file_status ("M");
|
|
} else {
|
|
file_status ("O");
|
|
}
|
|
}
|
|
}
|
|
|
|
# Now do directories.
|
|
unless ($no_recurse) {
|
|
my $save_curr_dir = $curr_dir;
|
|
foreach $file (sort keys %subdirs) {
|
|
if ($subdirs{$file} == SUBDIR_FOUND) {
|
|
$curr_dir = $save_curr_dir;
|
|
file_status ("X");
|
|
} elsif ($subdirs{$file} == SUBDIR_CVS) {
|
|
process_arg ($save_curr_dir . $file)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Turn a glob into a regexp without recognizing square brackets.
|
|
sub glob_to_rx_simple ($)
|
|
{
|
|
my ($expr) = @_;
|
|
# Quote all non-word characters, convert ? to . and * to .*
|
|
$expr =~ s/(\W)/\\$1/g;
|
|
$expr =~ s/\\\*/.*/g;
|
|
$expr =~ s/\\\?/./g;
|
|
return $expr;
|
|
}
|
|
|
|
# Turn a glob into a regexp
|
|
sub glob_to_rx ($)
|
|
{
|
|
my $result = '';
|
|
my ($expr) = @_;
|
|
# 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;
|
|
}
|
|
|
|
sub Main ()
|
|
{
|
|
# 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"
|
|
);
|
|
|
|
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
|
|
my $want_msg = 0; # List possible filetypes and exit
|
|
my $want_help = 0; # Print help and exit
|
|
my $want_ver = 0; # Print version and exit
|
|
|
|
my %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 ($#ARGV < 0) {
|
|
@ARGV = ("");
|
|
}
|
|
|
|
foreach (@ARGV) {
|
|
process_arg ($_);
|
|
}
|
|
|
|
if ($#batch_list >= 0) {
|
|
do_batch();
|
|
}
|
|
}
|
|
|
|
Main();
|