#!/usr/bin/perl # Copyright (c) 2001 "Brandon L. Golm" # All rights reserved. # # This program is free software; you can redistribute it and/or modify it # under the same terms as Perl itself provided this message and author # information are retained. # Note the embedded modules (that may, or may not, end up being further # developed and added to CPAN -- under different names). # Thereby: this script only requires a standard Perl and CVS install. $VERSION='0.9'; package RefWatch; require Carp; $VERSION = 0.01; sub new { my $proto = shift; my $meclass = ref($proto) || $proto; my $class = $meclass; if (my $bclass = shift) { eval "\@RefWatch::${bclass}::ISA = ('$meclass')"; $class = "${meclass}::${bclass}"; } my $a = 1; my $r = \$a; bless($r,$class); return $r; } sub DESTROY { my $s = shift; $s =~ /(?:.+?)::(.*)=/; Carp::carp("A $1 was destroyed without explicit close, possible data loss") if ${$s}; } package PrintTable; use Carp; $DEF_FORMAT = { align=>'left', cell=>[], }; $VERSION = 0.01; sub new { my $proto = shift; my $class = ref($proto) || $proto; my %param = @_; if (my $use = delete $param{'-use'}) { croak "can not load table view '$use': $@" if $@; my @ret = new {"PrintTable::$use"}(%param); croak "$@" if $@; return wantarray ? @ret : $ret[0]; } my $self = [{ nl => defined $param{'-nl'} ? delete $param{'-nl'} : "\n", queue => [], outf => delete $param{'-outf'} || *STDOUT, watch => new RefWatch($class), }, {%{$DEF_FORMAT}} ]; bless ($self, $class); return $self; } sub set_new { my $self = shift; my %param = @_; my $self0 = $self->[0]; @{$self0->{'queue'}} = (); $self0->{'nl' } = delete $param{'-nl' } if defined $param{'-nl' }; $self0->{'outf'} = delete $param{'-outf'} if defined $param{'-ouf' }; $self0->{'cols'} = delete $param{'-cols'} if defined $param{'-cols'}; return $self; } sub queue { my $self = shift; my $q = $self->[0]{'queue'}; foreach my $c (@_) { ref($q->[-1]) || push(@{$q},[]); push(@{$q->[-1]},$c); push(@{$q},[]) if $self->[0]{'cols'} <= @{$q->[-1]}; } return $self; } sub flush { my $self = shift; return if $self->[0]{'flushing'}; $self->[0]{'flushing'} = 1; while (my $row = shift @{$self->[0]{'queue'}}) { $self->row(@{$row}); } $self->[0]{'flushing'} = 0; return $self; } sub open { my $self = shift; ${$self->[0]{'watch'}}++ unless ${$self->[0]{'watch'}}; return $self; } sub close { my $self = shift; ${$self->[0]{'watch'}}-- if ${$self->[0]{'watch'}}; $self->flush(); return $self; } sub cell { my $self = shift; $self->queue(shift) while defined $_[0]; return $self; } sub AUTOLOAD {return shift} # silently ignore features missing # from different display drivers sub DESTROY { } sub header_row { my $self = shift; if ($_[0]) { $self->[0]{'header_row'} = [@_]; } else { return $self->row(@{$self->[0]{'header_row'}}); } return $self; } sub nl { my $self = shift; my $f = $self->[0]{'outf'}; print $f $self->[0]{'nl'}; return $self; } sub format { my $self = shift; my ($t,$fmt) = @{$self}; $fmt = &data_copy($fmt); my $cfmt = $fmt->{'cell'}; carp "format called with odd number of parameters!" if @_ % 2 && $^W; my $in; while ($in = shift) { if ($in eq 'cell') { my $ar = shift; local $_; for (ref($ar)) { if (/ARRAY/) { foreach my $i (0 .. $#{$ar}) { foreach my $k (keys %{$ar->[$i]}) { $cfmt->[$i]->{$k} = $ar->[$i]->{$k}; } } } elsif (/HASH/) { foreach my $i (keys %{$ar}) { foreach my $k (keys %{$ar->{$i}}) { $cfmt->[$i]->{$k} = $ar->{$i}->{$k}; } } } else { croak "format cell called with ${\($_||'SCALAR')} instead of HASH or ARRAY"; } } } else { $fmt->{$in} = shift; } } return (bless([$t,$fmt], $self =~ /(.*)\=/ && $1)); # self gets reconstructed after copy and edit. } sub data_copy { local $_; my @r; foreach my $e (@_) { for (ref($e)) { if(/ARRAY/) { push @r, [&data_copy(@{$e})]; } elsif (/HASH/) { push @r, {&data_copy(%{$e})}; } elsif (/SCALAR/) { my $s = &data_copy($$e); push @r,\$s; } else { push @r,$e; } } } return wantarray ? @r : $r[0]; } package PrintTable::Text; @ISA=('PrintTable'); $VERSION = 0.01; sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = $class->SUPER::new(@_); bless ($self, $class); return $self->set_new(@_); } sub set_new { my $self = shift; $self->SUPER::set_new(@_); my %param = @_; if (ref($param{'-width'}) eq 'ARRAY') { $self->[0]{'width'} = $param{'-width'}; } $self->[0]{'cellwrap' } = $param{'-cellwrap'} || 0; $self->[0]{'colpad' } = defined $param{'-colpad' } ? $param{'-colpad' } : ' '; $self->[0]{'underline'} = defined $param{'-underline'} ? $param{'-underline'} : '-'; $self->[0]{'indent' } = defined $param{'-indent' } ? $param{'-indent' } : ''; return $self; } sub row { my $self = shift; my ($self0, $fmt) = @{$self}; $self->flush; my $f = $self0->{'outf'}; my $cfmt = $fmt->{'cell'}; unless($self0->{'width'}) { my $w = $self0->{'width'} = []; foreach my $c (@_) { push(@{$w}, length($c)+1); } } my $w = $self0->{'width'}; my $colpad = $self0->{'colpad'}; local $^W = 0; local $_; my $redo = 1; my ($l,$cl,$algn,$spc); my @arr = (@_); while ($redo) { $redo = 0; print $f $self0->{'indent'}; foreach my $i (0 .. scalar @arr) { $l = length($arr[$i]); $cl = $w->[$i]; # col length $redo++ if $l > $cl; $algn = defined $cfmt->[$i]->{'align'} ? $cfmt->[$i]->{'align'} : $fmt->{'align'}; $spc = $cl-$l; print $f $l == $cl ? $arr[$i] : $l > $cl ? substr($arr[$i],0,$cl,'') : $algn eq 'right' ? (' 'x($spc), $arr[$i]) : $algn eq 'center' ? (' 'x(int(($spc)/2)+(($spc)%2)),$arr[$i], ' 'x(int(($spc)/2))) : ($arr[$i], ' 'x($spc)); $arr[$i] = '' unless $l > $cl; print $f $colpad; } $redo = 0 unless $self0->{'cellwrap'}; print $f $self0->{'nl'}; } return $self; } sub header_row { my $self = shift; my ($self0, $fmt) = @{$self}; my $f = $self0->{'outf'}; if ($_[0]) { $self0->{'header_row'} = [@_]; } else { $self->flush; # use map so that values are copied. $self->row(map {$_} @{$self0->{'header_row'}}); my $f = $self0->{'outf'}; my $colpad = $self0->{'colpad'}; my $ul = $self0->{'underline'}; foreach my $w (@{$self0->{'width'}}) { print $f $ul x($w); print $f $colpad; } print $f $self0->{'nl'}; } return $self; } sub close { my $self = shift; $self->SUPER::close(@_); my $f = $self->[0]{'outf'}; return $self; } package main; $VERSION = 0.2; use File::Basename; unless (-d 'CVS') { warn "You are not in a CVS checked out directory, you will get strange results!\n\n"; } $FILE_PREPEND = $ENV{'CVSLS_FILE_PREPEND'}; $CONFIG{'look_tags'} = 0; $CONFIG{'wor'} = 'repos'; #working or repos $CONFIG{'delim'} = ' '; my %WIDTH = (local => 8, working => 5, repos => 5); # defaults/minimums &check_flags; if ($ENV{'CVSLS_HEADER_EVERY'} =~ /^(\d+)$/) { $CONFIG{'header_every'} = $1 || 1; } elsif (`stty -a` =~ /; (\d+) rows;/) { $CONFIG{'header_every'} = $1 - ($CONFIG{'recurse'} ? 3 : 2); } else { $CONFIG{'header_every'} = 50; } sub check_flags { local $_; while ($ARGV[0] =~ /^-/) { $_ = shift(@ARGV); study; /^-{1,2}h(?:elp)?(?:(?:[:=])(.+))?$/i && do { &usage($1); exit; }; s/^-d// && do { $CONFIG{'delim'} = $_; next; }; /^-+hide-empty-dirs/ && do { $CONFIG{'hide_empty'}++; next; }; s/^-PT[:=]?// && do { do { warn qq(Only specify one "pretend tag"\n); &usage; exit; } if defined $CONFIG{'pretend_tag'}; $CONFIG{'pretend_tag'} = $_; $CONFIG{'pretend_tag'} =~ /\w/ || die "must specify pretend tag."; next; }; /^-HL$/ && do { $CONFIG{'hide_local'} = 1; next; }; /^-[Rr]$/ && do { $CONFIG{'recurse'} = 1; next; }; s/^-WT(Q)?[:=]// && do { $CONFIG{'quit_after_find'} ||= $1 ? 1 : 0; my $wild = $_; local $_; for (split(/,/,$wild)) { s/\*/.*/g; s/\?/\./g; $_ = "\^\Q$_" . '$'; #' s/\\\.\\\*/\.\*/g; s/\\\./\./g; $CONFIG{'wildcard_tags'}->{$_} = 1; } $CONFIG{'all_tags'} ||= {}; next; }; s/^--?(Q)?rxtag(Q)?[:=]?// && do { $CONFIG{'quit_after_find'} ||= ($1||$2) ? 1 : 0; my $regx = $_; local $_; for (split(/,/,$regx)) { $CONFIG{'wildcard_tags'}->{$_} = 1; } $CONFIG{'all_tags'} ||= {}; next; }; /^--?files$|^--$/ && last; s/^-T(Q)?(:|=)?// && do { $CONFIG{'my_tag_order'} = 1 if $2 eq '='; $CONFIG{'quit_after_find'} ||= $1 ? 1 : 0; if ($_) { @LOOK_TAGS = split /,/; %LOOK_TAGS = map { $_ => 1 } @LOOK_TAGS; $CONFIG{'look_tags'} = scalar @LOOK_TAGS; } else { $CONFIG{'all_tags'} ||= {}; $CONFIG{'find_tags'} = 1; } next; }; /^--?working$/ && do { $CONFIG{'wor'} = 'working'; next; }; /^--?cmpall$/ && do { $CONFIG{'cmpall'} = 1; next; }; s/^--?last-author(:|=)?// && do { $CONFIG{'show_author'} = 1; if ($_) { $CONFIG{'author_eq'} ||= {}; foreach my $author (split(/,\s*/)) { $CONFIG{'author_eq'}->{$author}++; } } next; }; /--?sticky[-_]options?[:=]?/ && do { $CONFIG{'show_sticky_opts'} = 1; next; }; s/^--?kosher[:=]?// && do { /.+/ || die "when you use -kosher=tag, specify a tag.\n"; $CONFIG{'kosher'} = $_; next; }; /-+.+/ && do { die "unknown option: $_\n"; } } } if ($CONFIG{'kosher'} && ! $CONFIG{'find_tags'}) { unless ($LOOK_TAGS{$CONFIG{'kosher'}}) { unshift(@LOOK_TAGS, $CONFIG{'kosher'}); $LOOK_TAGS{$CONFIG{'kosher'}} = 1; $CONFIG{'look_tags'}++; } } my $st; my $log_info; { my $command; my $log_command; { #local $|=1; #print "Querying CVS server . . ."; local @ARGV = @ARGV; @ARGV = map {s/^-/\\\\-/; s~/$~~; $_} @ARGV; my $recurse = $CONFIG{'recurse'} ? '' : 'l'; $command = scalar @ARGV ? "cvs status -v$recurse ${\(join' ',map{quotemeta}@ARGV)} 2>&1" : "cvs status -v$recurse 2>&1"; if ($CONFIG{'show_author'}) { if ($CONFIG{'author_eq'}) { my $auth = $CONFIG{'author_eq'} && join ',', keys %{ @CONFIG{'author_eq'} }; $log_command = scalar @ARGV ? "cvs log -w$auth -N$recurse ${\(join' ',map{quotemeta}@ARGV)} 2>&1" : "cvs log -w$auth -N$recurse 2>&1"; } else { $log_command = scalar @ARGV ? "cvs log -N$recurse ${\(join' ',map{quotemeta}@ARGV)} 2>&1" : "cvs log -N$recurse 2>&1"; } } #@cvs_status = scalar @ARGV ? `cvs status -vl ${\(join' ',map{quotemeta}@ARGV)} 2>/dev/null` # : `cvs status -vl 2>/dev/null`; #print "\r \r"; #select(undef,undef,undef,.15); } { my ($dir, $name, $tags, $tg, $bor, $rv, $branch_rev); $dir = '.'; local $/ = "===================================================================\n"; if ($CONFIG{'show_author'}) { my %h; for (`$log_command`) { /^RCS file: (.+)$/mg; my $file = $1; while (/^revision (\d+(?:\.\d+)+)\b/mg) { my $r = $1; /author: (.+?);/mg; ($h{$file}{'author'}{$r}) = $1 } } $log_info = \%h; } for (`$command`) { die "CVS error: $_\n" if /cvs.+abort/; /File: (?:no file )?(.+?)\s+Status: (?:\S+(?:\s\S+)?)/; if ($1) { $name = $1; } else { $dir = do { my @nd = /: Examining (\S+)/g; $nd[-1] ne '' ? $nd[-1] : $dir; }; next; } my $h = $st->{$dir}->{$name} = {}; ( $h->{'status' } ) = /Status: (\S+(?:\s\S+)?)/; ( $h->{'working'} ) = /Working revision:\s+(\d\S*)/; ( $h->{'repos' }, $h->{'file'}) = /Repository revision:\s+(\d\S*)\s+(.+)/; ( $h->{'sticky' }, $branch_rev ) = /Sticky Tag:\s+(\S+) (\(.+?\))/; if ($CONFIG{'show_sticky_opts'}) { ( $h->{'stick_opts'} ) = /Sticky Options:\s+\((.+)\)/; $ALL_STICKY_OPTS{$h->{'stick_opts'}}++; } { my $w = length($h->{'status'}); $WIDTH{'status' } = $w if $w > $WIDTH{'status'}; $w = length($h->{'working'}); $WIDTH{'working'} = $w if $w > $WIDTH{'working'}; $w = length($h->{'repos'}); $WIDTH{'repos' } = $w if $w > $WIDTH{'repos'}; } if ($CONFIG{'show_author'} && $h->{'repos'}) { my $author = $log_info->{ $h->{'file'} }{'author'}{ $h->{'repos'} }; $CONFIG{'all_authors'}->{$author}++; $h->{'author'} = $author; } delete $st->{$dir}->{$name} && next unless $h->{'repos'}; $h->{'bor'} = $branch_rev =~ /revision/ ? 'r' : $branch_rev =~ /branch/ ? 'b' : ''; ( $h->{'stickyrev'} ) = $branch_rev =~ /: (.+?)\)/; ( $tags ) = /Existing Tags:(.+)\n\n/s; $tags = '' if $tags =~ /No Tags Exist/; if ($CONFIG{'pretend_tag'} && $tags) { ( $h->{'sticky'}, $h->{'bor'}, $h->{'stickyrev'} ) = $tags =~ /\s+($CONFIG{'pretend_tag'})\s+\((\w)(?:\w+): (.+?)\)/o; $h->{'repos'} = $h->{'stickyrev'}; #next unless $h->{'sticky'}; #we're pretending to be a tag and this file #wouldn't exist in this directory. } foreach my $line (split(/\n/,$tags)) { ( $tg, $bor, $rv ) = $line =~ /^\s+(\S+)\s+\((\w)(?:\w+): (.+?)\)/; next unless $CONFIG{'all_tags'} || defined $LOOK_TAGS{$tg}; $CONFIG{'all_tags'}->{$tg}++ if $CONFIG{'all_tags'}; $h->{'tags'}->{$tg} = $h->{'bor'} ? $bor eq $h->{'bor'} ? $h->{'sticky'} ? $tg eq $h->{'sticky'} ? 'T' : (map { $_ == 0 ? '=' : $_ > 0 ? 'N' : 'O' } tag_cmp($rv, $h->{'stickyrev'}))[0] : $CONFIG{'cmpall'} ? (map { $_ == 0 ? '~' : $_ > 0 ? '^' : 'v' } tag_cmp($rv,$h->{$CONFIG{'wor'}}))[0] : $bor : $CONFIG{'cmpall'} ? (map { $_ == 0 ? '~' : $_ > 0 ? '^' : 'v' } tag_cmp($rv,$h->{$CONFIG{'wor'}}))[0] : $bor : $bor eq 'b' ? $CONFIG{'cmpall'} ? (map { $_ == 0 ? '~' : $_ > 0 ? '^' : 'v' } tag_cmp($rv,$h->{$CONFIG{'wor'}}))[0] : 'b' : (map { $_ == 0 ? '-' : $_ > 0 ? 'n' : 'o' } tag_cmp($rv, $h->{$CONFIG{'wor'}}))[0] } $dir = do { my @nd = /: Examining (\S+)/g; $nd[-1] ne '' ? $nd[-1] : $dir; }; die "CVS error: $_\n" if /cvs.+abort/; } } } if ($CONFIG{'kosher'}) { unless (defined($CONFIG{'all_tags'}->{$CONFIG{'kosher'}}) || defined($LOOK_TAGS{$CONFIG{'kosher'}})) { local $| = 1; print "I didn't find the tag: $CONFIG{'kosher'}, should I continue? [n] (y/n) > "; my $ans = ; exit unless $ans =~ /^y(e|es)?$/; delete $CONFIG{'kosher'}; } } if ($CONFIG{'find_tags'}) { @LOOK_TAGS = sort tag_cmp grep {!/^$/} keys %{$CONFIG{'all_tags'}}; %LOOK_TAGS = map { $_ => 1 } @LOOK_TAGS; $CONFIG{'look_tags'} = scalar @LOOK_TAGS; } elsif ($CONFIG{'wildcard_tags'}) { # both find_tags and wildcard_tags is useless # because find_tags will find all. my $temp; my @nlook_tags = grep {!/^$/} map { $temp = $_; (scalar grep { $temp =~ /$_/ } keys %{$CONFIG{'wildcard_tags'}}) ? $temp : '' } keys %{$CONFIG{'all_tags'}}; @LOOK_TAGS = $CONFIG{'my_tag_order'} ? ((sort tag_cmp @nlook_tags),@LOOK_TAGS) : (sort tag_cmp (@nlook_tags,@LOOK_TAGS)); %LOOK_TAGS = map { $_ => 1 } @LOOK_TAGS; $CONFIG{'look_tags'} = scalar @LOOK_TAGS; } my %ask_files; my $ask_files; my %ask_all_files_dirs; { my ($dir, $f); foreach my $file (@ARGV) { $ask_files++; if (-d $file) { $file =~ s~/$~~; $ask_all_files_dirs{$file}++; next; } ($f, $dir) = fileparse($file); $dir =~ s~^\./$~\.~; $dir =~ s~/$~~; if ($f eq '') { push(@other_ask_dirs, $dir); } else { $ask_files{$dir}->{$f}++; } } } my %files; { if ($CONFIG{'recurse'}) { if ($ask_files) { foreach my $dir (keys %ask_files, keys %ask_all_files_dirs) { opendir(DIR,$dir) || next; #die "can't open dir: $dir, $!"; my %n_files = map { $_ => 1 } readdir(DIR), keys %{$st->{$dir}}, keys %{$ask_files{$dir}}; @{$files{$dir}} = sort tag_cmp keys %n_files; closedir(DIR); } } else { my @chk_dir = ('.'); my $dir; while ($dir = shift(@chk_dir)) { opendir(DIR,$dir) || die "can't open dir: $dir, $!"; my %n_files = map { $_ => 1 } grep { !/(^\.\.?$)|(^CVS$)/ } readdir(DIR), keys %{$st->{$dir}}; foreach my $file (keys %n_files) { if (-d "$dir/$file") { $dir eq '.' ? unshift(@chk_dir,$file) : unshift(@chk_dir,join('/',$dir,$file)); $n_files{$file} = 0; } @{$files{$dir}} = sort tag_cmp grep { $n_files{$_} } keys %n_files; } closedir(DIR); } } } else { opendir(DIR,'.') || die "can't open dir: $!"; my %n_files = map { $_ => 1 } readdir(DIR), keys %{$st->{'.'}}, @ARGV; @{$files{'.'}} = sort tag_cmp keys %n_files; closedir(DIR); } } unless ($CONFIG{'file_width'}) { #find longest name my $l=1; my $tl; foreach my $name (map { @{$_} } values %files) { $tl = length($name); if ($tl > $l) { $l = $tl; } } $CONFIG{'file_width'} = $l; } if ($CONFIG{'show_author'}) { my $l=1; my $tl; foreach my $name (keys %{$CONFIG{'all_authors'}}) { $tl = length($name); $l = $tl if $tl > $l; } $CONFIG{'show_author'} = $l; push(@OTHER_COLS, 'author'); push(@OTHER_COL_HEADS, 'Author'); push(@OTHER_COL_WIDTHS, $l); } if ($CONFIG{'show_sticky_opts'}) { my $l=1; my $tl; foreach my $name (keys %ALL_STICKY_OPTS) { $tl = length($name); $l = $tl if $tl > $l; } $l = 4 if 4 > $l; push(@OTHER_COLS, 'stick_opts'); push(@OTHER_COL_HEADS, 'Opts'); push(@OTHER_COL_WIDTHS, $l); } my $t = new PrintTable(-use => 'Text', -cols => 4 + $CONFIG{'look_tags'}, -width => [ map { $_+1 } @WIDTH{qw/status working repos/}, @OTHER_COL_WIDTHS, @LOOK_TAGS{@LOOK_TAGS}, $CONFIG{'file_width'} ], -colpad => $CONFIG{'delim'}, ); { my $i = 1; my $x = 1; my $temp_table = $CONFIG{'look_tags'} ? new PrintTable(-use => 'Text', -cols => 3, -width => [qw/30 30 30/],) : undef; $t->header_row('Status','Local','Repository', (@OTHER_COL_HEADS), (map { $temp_table->cell("$i: $_"); $x = substr($i,-1); $i++; $x } @LOOK_TAGS), 'File Name'); $temp_table->close if $temp_table; if ($CONFIG{'quit_after_find'}) { $t->close; exit; } $t->header_row unless $CONFIG{'header_every'}; } my $kosher = $CONFIG{'kosher'} if defined $CONFIG{'kosher'}; { my $line = 0; my $header_line = 1; my $last_was_dir = 0; my $hide_empty = $CONFIG{'hide_empty'}; foreach my $dir (sort tag_cmp keys %files) { my $printed_dir=0; if ($CONFIG{'recurse'}) { $t->flush; print "\n" unless $last_was_dir++; print "Directory: $dir\n" unless $hide_empty; } for (@{$files{$dir}}) { next if -d "$dir/$_"; $t->header_row if $CONFIG{'header_every'} && $header_line-- == 1; $ask_files && next unless $ask_files{$dir}->{$_} || $ask_all_files_dirs{$dir}; if (my $h = defined $st->{$dir}->{$_} && $st->{$dir}->{$_}) { next if ($kosher && defined $h->{'tags'}->{$kosher} && $h->{'tags'}->{$kosher} =~ /[-=~T]/); next if ($CONFIG{'author_eq'} && ! $CONFIG{'author_eq'}->{$h->{'author'}}); print "Directory: $dir\n" if ($hide_empty && ! $printed_dir++); $t->row($h->{'status'}, -f "$dir/$_" ? ($h->{'working'} eq $h->{'repos'}) ? ' ->' : $h->{'working'} : '<-', $h->{'repos'}, (map { $h->{$_} } @OTHER_COLS), (map { $h->{'tags'}->{$_} || '' } @LOOK_TAGS), $_); $header_line = 1 unless ++$line % $CONFIG{'header_every'}; $last_was_dir=0; } elsif (! $CONFIG{'hide_local'}) { print "Directory: $dir\n" if ($hide_empty && ! $printed_dir++); $t->row('','','', (map { '' } @OTHER_COLS, @LOOK_TAGS), $_); $header_line = 1 unless ++$line % $CONFIG{'header_every'}; $last_was_dir=0; } } } } $t->close; sub tag_cmp { if ($_[1]) { $a = shift; $b = shift; } return 0 unless $a cmp $b; my @a = split(/(\D)/,$a); my @b = split(/(\D)/,$b); my $x = $#a > $#b ? $#b : $#a; my $cmp; foreach my $i (0 .. $x) { $cmp = $a[$i] =~ /^\d+$/ && $b[$i] =~ /^\d+$/ ? $a[$i] <=> $b[$i] : $a[$i] cmp $b[$i]; return $cmp if $cmp; } return $#a <=> $#b; } sub usage { my $what = shift; my %help = ( version => "This is cvsls version $VERSION with embedded modules. Please watch for future releases\n", kosher => " The 'kosher' option was inpired by cvs_kosher.pl by Pradeep Chetal. You have to check out your files as HEAD ('cvs co -r sometag' i.e. Checking out as a tag, or using -PT will be unproductive. The tag that you are comparing with kosher should be the latest tag, the use is to make sure the development HEAD versions are all tagged with latest tag.) Then to look for files that are out of sync with the tag in which you are interested, do one of the following: 1) csvls -WT:sometag-* -r -kosher:sometag-1-9-0 -HL |less Explanations: 1) -WT:sometag-* look for all tags that match that -r recurse subdirectories -kosher:sometag-1-9-0 show which files are different in HEAD than 'sometag-1-9-0' If you thought about it long enough, you may be able to produce good results using -PT and -working. ", ); my $help = join(', ', keys %help); if ($what) { if (defined $help{$what}) { print $help{$what}; } else { print "I cannot offer help on '$what', I only have specific help for:\n"; print "\t",join(', ',keys %help), "\n\n"; } } else { print <<"END"; Usage: $0 [OPTIONS...] [-files] [files...] OPTIONS: [-PT:] [-HL] [-WT | -WTQ ] [-T | -TQ ] cvsls: "The developers paradise" --h --help -help -he --hel -hel etc. this message. -h:xxx etc... -help:xxx Get specific help on any of these commands: $help -d' ' Delimiter (like "cut -d' '" except in the opposite direction) -hide-empty-dirs -R -r Be recursive (look at sub directories) -PT Pretend to be some tag (only one allowed) $0 -PT:sometag -HL Hide files that are only found locally. -WT Look for wildcard tags: $0 -WT:sometag*,tag??end -rxtag Look for tags based on Perl regular expressions: (NOTE: -WT automatically creates this assertion: /^wilcard\$/ -rxtag does not.) $0 -rxtag:xxxxx (i.e. some regular expression, perldoc perlre) -T Look for tags: $0 -T:sometag,othertag file1 file2 $0 -T=sometag,othertag file1 file2 ('=' causes order specified to be retained tags will be appended to the end sorted list of tags found by -WT if any) Look for ALL tags: $0 -T Q may be specified after -WT (-WTQ) -rxtag (-Qrxtag, -rxtagQ) -T (-TQ) to print the tags that were found, but not the files. -working Compare to the working (local) version of the file. (Default action is to compare the MAIN version of the file.) -- Forces everything after to be interpereted as file names. -files Forces everything after to be interpereted as file names. -cmpall | (use carefully) Forces file version | comparison between 'revision' and 'branch' tags. | | The indicators, (see LEGEND) in lieu of 'b' and 'r' are: | '~' for same, | '^' for newer, | 'v' for older. | | (One tip: [ - - ^ - ] will be misleading when '^' marks | a branch file that was never modified. This file version appears | newer, which may or may not be the case, depending on if the | file was modified and whether the branch was merged. A properly | commited modified branch file should look like [ o o ~ - ] ) L________________________________________ -kosher:sometag Print only files that aren't exactly tagged for this tag. Works great with -HL. -sticky-options Show the 'Sticky Options' associated with the file. ___alpha features___ | | These two features are implemented badly. You need to set CVSLS_FILE_PREPEND | for them to work. I will remove or change them in the future (they were hacked | in for one person, and it wasn't done correctly). | | -last-author Print the name of the last person to | check in each file (SLOW) | | -last-author=name1,name2 | Only show files that were last checked-in | by person(s). (SLOW) L________________________________________ TAG LEGEND: Compared to current tag: T This version of the file is TAGGED with this tag. = This version is the same in this other tag. N A newer version of this file exists with this other tag. O An older version of this file exists with this other tag. Generally: - The MAIN version of this file is the same as this tag has. n This file is newer in this tag than the MAIN version. o This file is older in this tag than the MAIN version. r This is a revision TAG file, which cvsls can't compare to your branch. b This is a branch TAG file, which cvsls can't compare to your revision. SUGGESTIONS: cvsls -WT:foo-1_5_* -T=V1_0,preV2_0 cvsls -PT:foo-1_5_12 -WT:foo-R1_5_* -T=V1_0,preV2_0 cvsls -WT:foo-* cvsls -rxtag:1_5_[0-3] Shows all tags marked with branch convention. ENVIRONMENT VARIABLES: CVSLS_HEADER_EVERY = x Print the header every 'x' number of lines. CVSLS_FILE_PREPEND Supports looking in the files to find authors. This is _alpha_, and will be removed in the future. AUTHOR "Brandon L. Golm" END } } __END__ =head1 NAME cvsls - displays 'cvs status' in an configurable and easy to digest format. =head1 SYNOPSIS C for help. POD is here to point you there. =head1 AUTHOR "Brandon L. Golm" Please feel free to contact me with questions or problems. =head1 SCRIPT CATEGORIES VersionControl/CVS =head1 SEE ALSO cvs, mks2cvs =head1 README Displays 'cvs status' in an configurable and easy to digest format. See the 'cvsls -h' option for more help. This program is I because I want to clean up the documentation. Otherwise, there's nothing wrong with this code, and you shouldn't be scared of it. Please note that '-kosher' option is very usefull; but you should read C to see how to get started and what it might offer you. I accept feature requests and patches as long as the new code follows the same style as this code or C. Color support is one idea I already want to work on. =head1 COPYRIGHT This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself provided this message and author information are retained. (BUT PLEASE LET ME KNOW IF YOU DO!) =cut