#!/bin/sh exec perl -S -x -- "$0" "$@" #!perl -w use English; # Despite the claims of the manual, $ARG is an unreliable alias for $_ # after executing "*ARG = *_", which is what "use English" does. # There are strange interactions with the "foreach" statement. I # think this used to work properly in an older version of Perl. Or # maybe it's a newer version that works. use strict; use Getopt::Long; use File::Find; # TODO: There is some bug which prevents this script from dying when # interrupted from keyboard with Control-C. Fix. Eeek. It turns out # this is a feature of the "system" function. # (man "perlvar") # (man "File::Find") use JBW::Utils qw(&with_file_contents &get_file_contents); sub show_help { print STDERR "Usage: $PROGRAM_NAME [--help | --version | --cvs-program=PROG | directory]\n"; my $short_prog_name = $PROGRAM_NAME; $short_prog_name =~ s(.*/)(); print STDERR "Try `perldoc $short_prog_name' for more information.\n"; print STDERR "If that fails, try `perldoc $PROGRAM_NAME'.\n"; print STDERR "If the output from perldoc looks unreadable, try `perldoc -t $PROGRAM_NAME'.\n"; } my ($Show_Help, $CVS_Program); $CVS_Program = 'cvs'; if (! GetOptions ("help" => \$Show_Help, "cvs-program=s" => \$CVS_Program)) { show_help(); exit 1; } show_help() if $Show_Help; exit 0 if ($Show_Help); my (%IgnoreHost); if (defined $ENV{'UPDATE_IGNORE_HOSTS'}) { grep {$IgnoreHost{$_} = 1;} split (' ', $ENV{'UPDATE_IGNORE_HOSTS'}); } my ($IgnoreSVNURLPattern); if (defined $ENV{'UPDATE_IGNORE_SVN_URL_PATTERN'}) { $IgnoreSVNURLPattern = $ENV{'UPDATE_IGNORE_SVN_URL_PATTERN'} }; # *** add darcs #foreach my $key (keys %IgnoreHost) { # print "key: [$key]\n"; } # For debugging: $OUTPUT_AUTOFLUSH = 1; # *** add darcs print "Finding CVS, Subversion, and Git working directories.\n"; my $StartDir; if ($#ARGV == 0) { $StartDir = "$ARGV[0]"; } else { $StartDir = "$ENV{'HOME'}"; } if (-l $StartDir) { $StartDir = "$StartDir/."; } # *** add darcs my (%CVS_Working_Dir, %CVS_Host, %Subversion_Working_Dir, %Git_Dir); #print STDERR "StartDir: [$StartDir]\n"; sub make_single_quote_safe { my $x = shift; $x =~ s/\'/\'\\\'\'/; return $x; } File::Find::find (sub { #print STDERR "_: [$_]\n"; # The absolute name of the current file (or its name # relative to the current directory of this program when it # started if the top-level find targets are relative). # Same as "$File::Find::dir/$_". (There is a small # insignificant exception to this in the case of the # top-level find targets.) my $SemiAbsName = "$File::Find::name"; if (-d $_) { if (-l $_) { print "Ignoring symlink to directory: [$SemiAbsName]\n"; $File::Find::prune = 1; return; } elsif (-f "$_/.updateignore") { print "Ignoring working directories in: ", "$SemiAbsName\n"; $File::Find::prune = 1; return; } elsif (-d "$_/CVS" && -f "$_/CVS/Root") { $File::Find::prune = 1; my $Host = with_file_contents "$_/CVS/Root", sub { if (! m/^(?:(?::pserver:)?(?:[^:@]+@)?([-.a-zA-Z0-9]+):)?(?:.+)$/) { die "bad CVS working dir: $SemiAbsName"; } # I think $1 will be undef if all of the optional # parts of the pattern are missing. #print STDERR "_: [$_]\n"; return $1; }; #print STDERR "_: [$_]; Host: [$Host]\n"; if (defined $Host) { if ($IgnoreHost{$Host}) { print "Ignoring working directories in: ", "$SemiAbsName\n"; return; } else { $CVS_Host{$Host}{$SemiAbsName} = 1; }} else { # Local repository. #print STDERR ("Root contents: [" . (get_file_contents("$_/CVS/Root")) . "]\n"); $CVS_Host{''}{$SemiAbsName} = 1; } #print "CVS working dir: $SemiAbsName\n"; $CVS_Working_Dir{$SemiAbsName} = 1; return; } elsif (-d "$_/.svn" && -f "$_/.svn/entries") { $File::Find::prune = 1; #system ('echo PWD; pwd'); # Our current directory is $File::Find::dir, and $_ is # the name of a subdirectory of the current directory. my $Dir_Safe = make_single_quote_safe ($_); my $Info = `cd '$Dir_Safe'; svn info`; if ($Info !~ m/^URL: (.*)$/m) { print STDERR "Bad Subversion working dir: $SemiAbsName\n"; print "Ignoring working directories in: $SemiAbsName\n"; return; } my $URL = $1; #print STDERR "URL: [$URL]\n"; if ((defined $IgnoreSVNURLPattern) && ($URL =~ m/$IgnoreSVNURLPattern/)) { print "Subversion URL matched by UPDATE_IGNORE_SVN_URL_PATTERN: $URL\n"; print "Ignoring working directories in: $SemiAbsName\n"; return; } $Subversion_Working_Dir{$SemiAbsName} = 1; # *** Distinguish SVN directories on local host ... # my $Host = with_file_contents "$_/.svn/entries", sub { ... }; # #print STDERR "_: [$_]; Host: [$Host]\n"; # if (defined $Host) { # if ($IgnoreHost{$Host}) { # print # "Ignoring Subversion working directories in: ", # "$SemiAbsName\n"; # return; } # else { # $Subversion_Host{$Host}{$SemiAbsName} = 1; }} # else { # # Local repository. # $Subversion_Host{''}{$SemiAbsName} = 1; } #print "Subversion working dir: $SemiAbsName\n"; } elsif (-d "$_/.git" && -f "$_/.git/index") { $File::Find::prune = 1; $Git_Dir{$SemiAbsName} = 1; } # *** add darcs }}, $StartDir); ## crude hack, not always correct #sub Containing_Dir { # my ($File) = @_; # if ($File =~ m|^(.+)/[^/]+/*$|) { # return $1; } # elsif ($File =~ m|^/[^/]+/*$|) { # return '/'; } # else { # print "File: $File\n"; # die "unhandled case"; }} print "top-level CVS working directories, sorted by repository host:\n"; foreach my $Host (sort (keys %CVS_Host)) { if ($Host eq '') { print " from repositories on the local host:\n"; } else { print " from repositories on $Host:\n"; } foreach my $Dir (keys %{$CVS_Host{$Host}}) { print " $Dir\n"; }} # *** TODO: Change following code to work host-by-host. # *** TODO: Change following code to start a shared master SSH # connection for each host with more than one working directory # checked out, if no shared master connection is already running for # that host. # *** TODO: Detect nesting of working directories with distinct remote # repositories (and especially hosts). Do something about it. (How # to stop CVS from automatically descending into such a working # directory?) my $max_columns = `tput cols`; if ($max_columns !~ m/^[0-9]+$/) { print STDERR "bad output from tput cols: [$max_columns]\n"; $max_columns = 80; } my $red_tty = `tput setaf 1 || echo bad`; my $blue_tty = `tput setaf 4 || echo bad`; my $restore_tty = `tput sgr0 || echo bad`; if (($red_tty =~ m/bad/) || ($blue_tty =~ m/bad/) || ($restore_tty =~ m/bad/)) { print STDERR "bad output from tput\n"; $red_tty = ''; $blue_tty = ''; $restore_tty = '';} sub print_divider { my $msg = shift; $msg = "$red_tty== $blue_tty" . $msg . " $red_tty" . ('=' x ($max_columns - 5 - (length $msg))) . "$restore_tty\n"; print STDERR ($red_tty, $msg, $restore_tty); } foreach my $Dir (sort (keys %CVS_Working_Dir)) { #print "Dir: $Dir\n"; print_divider "Updating CVS working directory: $Dir"; my $Dir_Safe = make_single_quote_safe ($Dir); if (-f "$Dir/.update_no_new_dirs") { system ("cd '$Dir_Safe'; $CVS_Program update"); } else { system ("cd '$Dir_Safe'; $CVS_Program update -d"); }} foreach my $Dir (sort (keys %Subversion_Working_Dir)) { #print "Dir: $Dir\n"; print_divider "Updating Subversion working directory: $Dir"; my $Dir_Safe = make_single_quote_safe ($Dir); system ("cd '$Dir_Safe'; svn update; svn status"); } foreach my $Dir (sort (keys %Git_Dir)) { #print "Dir: $Dir\n"; print_divider "Updating Git repository and working directory: $Dir"; my $Dir_Safe = make_single_quote_safe ($Dir); system ("cd '$Dir_Safe'; git-update-from-remotes"); } # *** add darcs =head1 NAME update - update all CVS and Subversion working directories =head1 SYNOPSIS B [B<--help> | B<--cvs-program=>I | I ] =head1 DESCRIPTION # *** add darcs This program updates all CVS and Subversion working directories inside a root directory, which is either passed as an argument (only one root directory may be specified at a time) or your home directory if no argument is provided. It finds every descendant of the root directory which is a CVS or Subversion working directory not immediately inside another such working directory. For each CVS working directory, it runs B in that directory, unless that directory contains a file named F<.update_no_new_dirs>, in which case it runs B. For each Subversion working directory found, it runs B and B. This program will ignore any subtree inside the root directory which contains a file named F<.updateignore>. The environment variable UPDATE_IGNORE_HOSTS can contain a space-separated list of host names. This program will not attempt to run B in a CVS working directory which is checked out from a host listed in UPDATE_IGNORE_HOSTS. The environment variable UPDATE_IGNORE_SVN_URL_PATTERN can contain a Perl regexp. This program will not attempt to update Subversion working directories whose URLs match this pattern. The options are: =over 4 =item B<--cvs-program=>I Use I instead of B as the name of the CVS program. (There is no option to replaced B.) =item B<--help> Show this documentation. =head1 DEPENDENCIES B uses the B module. =head1 AUTHORS Joe Wells (F) is the original author. Sebastien Carlier (F) added the option of using as the root directory a directory other than the home directory. =cut # Local variables: # mode: perl # end: