summaryrefslogtreecommitdiff
path: root/ndb/home/bin/cvschk
blob: 4510cc30888b7e83677fbb3d1ae8471a5f1b7943 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
#!/usr/bin/perl -w
#
# cvschk -- fast offline check for new files and modifications of files

# cvschk : A perl program which checks the status of the CVS controlled
#          files and gives an ASCII table sorted after the status of files.
#	   
#          If you have used CVS, then you know that it is hard to 
#          get a good overview the CVS-status of the files in you 
#          directories. Any new files? Any files changes? 
#          cvschk will help the programmer get the overview in the
#          situation, where we do not have access to the CVS repository.
#	   
#          Note that the program does only local checks of the files
#          If you have fast access to the CVS repositiory, then consider
#          the cvsstat-program - which additionally can tell if other
#          people have made newer versions of the files.
#
# The program requires Perl 5.004 (maybe previous versions also work).
#
# It is tuned to parse the output of cvs(1) version 1.9.
# Earlier and later versions may require modifications to the script.
#
# ** Note that the first line might be wrong depending **
# ** on the location of your perl program.             **
#
# Sample output:
# The directory ./mytempdir is not under CVS control
# 
# Changed files
# ---------------
# ./cvs2html
# ./cvschk
# ./cvsstat
# 
# New files
# ---------------
# ./.#cvschk
# ./XX
# ./cvs2html.ok
#  
# Deleted files
# ---------------
# (none)                                      

# Changelog:
#
# Ver   Date        Author                Changelog
# ---   ----------  --------------------  -------------------------------------
# 1.12  2002-01-04  Michael Kohne         Fixed a $foo=<> warning for 
#                                         5.004_01 with defined($foo=<>)
#                                         Added a --tabular|-t switch
#
# 1.11  2001-12-27  Michael Kohne         Added cvsignore functionality
#                                         Handling of 'dummy timestamp' 
#                                         Handling of 'Result of Merge'
#
# 1.10  2001-11-06  Michael Kohne	  Added -r and -l options
#
# 1.9   2001-08-03  Lars G. T. Jørgensen  Hack to allow special entry-line
#
# 1.8   2001-06-07  Peter Toft            Back to the same as 1.6
#                                         CVS is my friend
#
# 1.7   2001-06-04  Peter Toft            Peter was very tired and 
#                                         applied a wrong patch -
#                                         version 1.7 is crap
#
# 1.6   2000-12-17  Peter Toft            Better description added
#
# 1.5   2000-11-04  Peter Toft            URL of cvsstat changed
#
# 1.4   2000-09-20  Peter Toft            Must show deleted files also
#                                         as the default
#
# 1.3   2000-08-08  Ole Tange and         Initial version
#                   Peter Toft 
# ----  ----------  --------------------  -------------------------------------
#
# -----------------------------------------------------------------------------
#
# This program is protected by the GPL, and all modifications of
# general interest should be emailed to the maintainer (pto@sslug.dk).
#
# This program also uses code parts from cvsstat 
# (same homepage as cvschk)
#
# Copyright 2000,2001 by Peter Toft <pto@sslug.dk> and Ole Tange <ole@tange.dk>
#   as well as
#   Lars G. T. Jørgensen <larsj@diku.dk>
#
# The URL of the home page of cvschk is shown below.


use Time::Local;
use strict;
use Getopt::Long;

my $startdir = ".";

my $debug = 0;
my (%files,%filesok,%seen,%skip);


# Michael Kohne 12/16/01
#
# Simulation of .cvsignore as CVS does it...
#
# using .cvsignore handling makes cvschk take from 2 to 3 times
# longer to run over the same set of files.
# in my tests, disabling cvsignore altogether, cvschk takes .2 
# seconds on my working directory. Adding cvsignore,takes 
# .4 seconds. 
# Note that I do not use individual .cvsignore files - if there
# are a lot of them in your directory tree, it will add run time
#
# variables used for .cvsignore handling
my $initcvsignoreregex;# regex holding all startup cvsignore pattersn (no ())
my $cvsignoreregex;# one regex holding all current cvsignore patterns
my $disable_cvsignore=0;# set to 1 to disable cvsignore emulation
			# (available in case it's REALLY screwed up)
my $disable_ind_cvsignore=0;# set to 1 to disable finding .cvsignore files
			    # in each directory.
my $debug_cvsignore = 0; # For debugging .cvsignore problems

my %mon;
@mon{qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)}=
    0..11; # Perl months are 0 .. 11

my ($version)    = ('$Revision: 1.12 $ ' =~ /^\$\w+: (.*) \$ $/);
my $URL          = "http://cvs.sslug.dk/cvs2html";
my $version_line = "cvschk version $version (see $URL)\n";

my $opt_all;
my $restrict;
my $local;
my $tabular;

my $opt_restrict;

sub show_version {print $version_line}

sub die_version  {die $version_line}

sub die_usage {
    my $bundled = ($] > 5.00399
		   ? "can be bundled"
		   : "can't be bundled, because your Perl is too old");
    die <<END_OF_USAGE; # Help in the style of GNU `ls --help' or `make --help'
Usage: $0 [OPTION]... 
  Show the CVS status of FILEs (the current directory by default),
  traversing directories recursively and telling if new files exist
  in the repository.
Options:
  -a, --all         Show all statistics, including the names of files that
                    are up to date, used tags, ignored patterns and more
  -r, --restrict    Don't show the names of the unknown files
		     (useful if you have many temporary files)
  -t, --tabular     Show one file per line, each preceeded with a status word,
		     Sorted by filename.
  -l, --local       Don't descend into sub-directories
  -d, --debug       Debug info
  -h, --help        Show this help end exit immediately
  -V, --version     Show the version line and exit immediately
The one-letter options $bundled.
END_OF_USAGE
}

sub die_help {show_version; die_usage}

# Let `-ar' mean `-a -r' and require `--all' (or -a) instead of `-all'.
if ($] > 5.00399) { # This requires 5.004, so silently skip it for older Perls.
    eval {Getopt::Long::config("bundling")}; # avoid 5.003 compilation error
    warn $@ if $@; # For Perl 5.004+ we do want to see any compilation error
}


GetOptions( "all|a"      => \$opt_all,
            "tabular|t"  => \$tabular,
            "restrict|r" => \$restrict,
            "local|l"    => \$local,
	    "help|h"     => \&die_help,
	    "debug|d"    => \$debug,
	    "version|V"  => \&die_version,
	  ) or die_usage;

sub cvs_changed_in_dir($); #define prototype (for recursion)

# functions for .cvsignore handling

# converts a given filename pattern
# (of the sort that sh(1) takes) to
# a perl regex of similar meaning.
#
# It works by doing the following:
#
# change:
# . to \.
# $ to \$
# * to .*
# ? to .
# 
sub fpat_to_regex($)
{
  my $fexp;
  $fexp = shift;
  $fexp =~ s/\./\\\./g;#change . to \.
  $fexp =~ s/\$/\\\$/g;#change dollar sign to \dollar sign
  $fexp =~ s/\*/.*/g;# change * to .*
  $fexp =~ s/\?/./g; # change ? to .
  return $fexp;
}

# copy the input list to one single regex, 
# items seperated by | symbols. 
# return the regex string
sub do_regex_convert
{
  my $rx = "";
  my $first = 1;#true for first element only


  # convert each element of cvsignore into a regex    
  # this makes the patterns usable in perl
  my $cp;
  foreach $cp (@_) {
    if (not $first) { $rx = $rx . "|"; }
    if ($first) { $first = 0; }
    $rx = $rx . fpat_to_regex($cp);
  }

  return $rx;
}

# first parameter is a reference to the array
# to be loaded
# the rest of the parameters are just items
# that need to be loaded into the array.
# Note that if a ! is found, the list is 
# emptied, then further items are added.
# returns true if a ! was found
sub load_list_from_list
{
  my $arref = shift;# get reference to array from front
  my $item;
  my $ret=0;#false means no ! found
  
  chomp @_;#kill newlines
  foreach $item (@_) {
    $item =~ s/^\s*(.*?)\s*$/$1/;#kill leading/trailing whitespace
    if ($item) { # empty string is false
      push @$arref,$item;
    }
    if ($item eq "!") {
      @$arref = ();# '!' causes list to clear
      $ret = 1;# ! found
    }
  }
  
  return $ret;
}

# loads the given list with lines from the
# specified file. Note that if a '!' is found
# all prior patterns are removed from the list
# before the following patterns are loaded
# first param is the filename, 
# second param is a reference to an array
# that the data is to go into
# returns true if a ! was found
sub load_list_from_file
{
    my @inlist;
    my $fname = shift;#filename to read from
    #if (not -e $fname) { return; }
    my $arref = shift;#array to store into
    open CVSIGNORE,"$fname" or return;#file might not exist, that's OK
    @inlist = <CVSIGNORE>;
    close CVSIGNORE;
    return load_list_from_list($arref,@inlist);
}

# loads $cvsignoreregex from
# $initcvsignoreregex and the .cvsignore file 
# in the local directory
sub load_cvsignore
{
  if ($disable_ind_cvsignore) {return;}#don't look for local .cvsignore files
  if ($disable_cvsignore) {return;}#don't do anything

  my $dir = shift;
  my @cvsignore;

  # bang will be true if a ! was found. In such cases, I need
  # to not use the pre-exisitng regex list.
  my $bang = load_list_from_file("$dir/.cvsignore",\@cvsignore);
 
  # if we get a local cvsignore list, then...
  my $rx = do_regex_convert(@cvsignore);
  if ($rx) {
    $cvsignoreregex = "(";
    if (not $bang) {$cvsignoreregex = $cvsignoreregex . $initcvsignoreregex . "|";}
    $cvsignoreregex = $cvsignoreregex . $rx . ")";
  } else {
    if ($bang) {$cvsignoreregex = "";}
    else {$cvsignoreregex = "(" . $initcvsignoreregex . ")";}
  }

  if ($debug_cvsignore) {print $dir,":",$cvsignoreregex, "\n";}
}


# loads all of the cvsignore patterns that 
# can be loaded at script startup
sub load_initial_cvsignore()
{
    #load the default patterns
    # (taken from http://www.gnu.org/manual/cvs-1.9/html_node/cvs_141.html#IDX399)
    #
    # this gives you the patterns that cvs normally starts with
    my @initcvsignore;
    push @initcvsignore,("RCS");
    push @initcvsignore,("SCCS");
    push @initcvsignore,("CVS");
    push @initcvsignore,("CVS.adm");
    push @initcvsignore,("RCSLOG");
    push @initcvsignore,("cvslog.*");
    push @initcvsignore,("tags");
    push @initcvsignore,("TAGS");
    push @initcvsignore,(".make.state");
    push @initcvsignore,(".nse_depinfo");
    push @initcvsignore,("*~");
    push @initcvsignore,("\#*");
    push @initcvsignore,(".\#*");
    push @initcvsignore,("\,*"); 
    push @initcvsignore,("_\$\*");
    push @initcvsignore,("*\$");
    push @initcvsignore,("*.old");
    push @initcvsignore,("*.bak");
    push @initcvsignore,("*.BAK");
    push @initcvsignore,("*.orig");
    push @initcvsignore,("*.rej");
    push @initcvsignore,(".del-*");
    push @initcvsignore,("*.a");
    push @initcvsignore,("*.olb");
    push @initcvsignore,("*.o");
    push @initcvsignore,("*.obj");
    push @initcvsignore,("*.so");
    push @initcvsignore,("*.exe");
    push @initcvsignore,("*.Z");
    push @initcvsignore,("*.elc");
    push @initcvsignore,("*.ln");
    push @initcvsignore,("core");


    # now, load (in proper order!)
    # each of the possible cvsignore files

    # there are 4 possible .cvsignore files:
    
    # $CVSROOT/CVSROOT/cvsignore
    # ~/.cvsignore
    # $CVSIGNORE environment variable
    # .cvsignore in current directory

    # The first (CVSROOT/cvsignore) would require calling cvs, so
    # we won't do that one.
    # The last (.cvsignore in current directory) is done
    # for each directory. It's handled in the load_cvsignore routine.

    # ~/.cvsignore
    my @inlist;
    my $item;
    my $HOME=$ENV{"HOME"};
    if (not $HOME) {$HOME = ".";}
    load_list_from_file("$HOME/.cvsignore",\@initcvsignore);
    
    # $CVSIGNORE environment variable
    my $igstr = $ENV{"CVSIGNORE"}; # get env var
    if ($igstr) {
      my @iglist = split(/\s+/, $igstr); #if it exists, convert to list
      load_list_from_list(\@initcvsignore,@iglist);
    }
    
    # now that @initcvsignore is setup, 
    # turn it into a regex string
    $initcvsignoreregex = do_regex_convert(@initcvsignore);

    # now preset the cvsignore regex string to match
    # @initcvsignore. That way, if we aren't using local
    # cvsignore files, we do nothing.
    $cvsignoreregex = "(" . $initcvsignoreregex . ")";
}
# routine to see if the given name is in the cvsignore regex
# returns true if it is, false if it's not
sub ignore_file($)
{
  #allow user to disable the cvsignore stuff
  if ($disable_cvsignore) {return 0;}
  if (not $cvsignoreregex) {return 0;}# if regex is empty, nothing matches the regex
  my $filename = shift;
 
  if ($debug_cvsignore) {print "ignore_file:",$filename,"\n";}

  if ($filename =~ $cvsignoreregex) {
    if ($debug_cvsignore) {print $filename," matches\n";}
    return 1;
  }
  
  if ($debug_cvsignore) {print $filename," doesn't match\n";}
  return 0;
}

sub cvs_changed_in_dir($) {
  my $dir = shift;

  my ($line,$filename,$version,$mtime,$date,
      $dir_filename,$cvstime,@subdirs,
      @new_in_dir,$i);

  # Examine status of files in CVS/Entries
  if(not open(ENTRIES,"$dir/CVS/Entries")) {
      if ($tabular) {
          push @{$files{Unknown}}, $dir;
      }
      else {
          warn "The directory $dir is not under CVS control\n";
      }
  } else {
      load_cvsignore($dir);#load up proper cvsignore for given directory

      while(defined ($line=<ENTRIES>)) {
	  # Parse CVS/Entries-line
          $line=~m!^/(.*)/(.*)/(.*)/.*/! or do {
	      $debug and warn("Skipping entry-line $line"); 
	      next; 
	  };
	  ($filename,$version,$date) = ($1,$2,$3);
	  $dir_filename=$dir."/".$filename;
	  	  
	  # Mark this file as seen
	  $seen{$dir_filename}=1;
	  
	  # if not exists: Deleted
	  if(not -e $dir_filename) {
	      push @{$files{Deleted}}, $dir_filename; next;
	  }
	  # if dir: save name for recursion
	  -d $dir_filename and do { 
	      push @subdirs, $dir_filename; next; 
	  };
      
	  # modification time of $dir_filename
	  $mtime= (stat $dir_filename)[9];

       
          if($date eq "dummy timestamp") {
            # dummy timestamp means it's new to the repository.
            push @{$files{Changed}}, $dir_filename;
            if ($debug) {
		print "$dir_filename is changed\n";              
	    }
          }
          elsif($date eq "Result of merge") {
            # result of merge means it's changed, then updated.
            push @{$files{Changed}}, $dir_filename;
            if ($debug) {
		print "$dir_filename is changed\n";              
	    }
          }
	  elsif(not 
	      $date=~/... (...)\s+(\d+)\s+(\d+):(\d+):(\d+) (\d{4})/) 
	  {
	      #bogus entry in Entires
	      warn "Warning: $dir_filename -> '$date' ".
		  "not in ctime(3) format\n";
	  } else {
	      $cvstime=timegm($5,$4,$3,$2,$mon{$1},$6);
	      if($cvstime != $mtime) {
		  push @{$files{Changed}}, $dir_filename;
		  if ($debug) {
		      print "$dir_filename is changed\n";
		  }
	      } else {
		  push @{$files{Unchanged}}, $dir_filename;
		  if ($debug) {
		      print "$dir_filename is Unchanged\n";
		  }
	      }
	  }
      }
      close ENTRIES;
      
      # Locate any new files/dirs
      if(not opendir(D,$dir)) {
	  warn("Cannot open $dir");
	  @new_in_dir= ();
      } else { 
	  @skip{qw(. .. CVS)}=1..3; # Filenames that that we want to ignore
	  				#(note: these are exact filenames)
	  @new_in_dir= 
	      (grep { not $seen{$_} } # files we have not already processed
	       map { $dir."/".$_ }    # map from file to dir/file
	       grep { not ignore_file($_) } # ignore files in the cvsignore list
	       grep { not $skip{$_} } # skip files to be ignored
	       readdir(D));
	  closedir(D);
      }
      
      # Remember new files (actually non-directories)
      push @{$files{New}}, grep { not -d $_ } @new_in_dir;
      if ($debug) { print "@{$files{New}} are new in $dir\n"; }

      # Remember new subdirs
      push @subdirs, grep { -d $_ } @new_in_dir;
      
      # Recurse all subdirs
      if (not $local) {
	      for $i (@subdirs) {  cvs_changed_in_dir($i); } 
      }
  }
}

sub print_status()
{
    my $k;
    my %show_these_states = ("Changed" => 1);
    if(not $restrict) {
    	$show_these_states{"New"} = 1;
    	$show_these_states{"Deleted"} = 1;
    }

    if($opt_all) { $show_these_states{"Unchanged"} = 1; }

    if ($tabular) {
        my %allfiles;		# key: filesname, value: state
	my ($file, $state, $statefiles);

    	$show_these_states{"Unknown"} = 1;
	while (($state, $statefiles) = each %files) {
	    for my $f (@{$statefiles}) {
		$allfiles{$f} = $state;
	    }
        }
	for $file (sort keys %allfiles) {
	    $state = $allfiles{$file};
	    printf("%-10s %s\n", $state, $file) if $show_these_states{$state};
        }
    }
    else {
        print "\n";
    	for $k (keys %show_these_states) {
    	    if(not $files{$k} or not @{$files{$k}}) {
    		# no files
    		$files{$k}=["(none)"];
    	    }
    	    print("$k files\n",
    		  "---------------\n",
    		  map { "$_\n" } sort @{$files{$k}});
    	    print "\n";
    	}
    }
}

load_initial_cvsignore();
if ($debug_cvsignore) {print "initial regex:",$cvsignoreregex,"\n";}
cvs_changed_in_dir($startdir);
print_status();