#! /usr/bin/perl -w use strict; use Fcntl; # Tool based on Tim Hemel's code implements extra cvs target 'submit'. # It looks recursively for the changed files and commmits them in one operation. # Designed to be aliased to 'cvs'. # Vadim Belman # Anton Berezin # Dmitry Karasik ############################################################################# # CVS settings my $CVS_CMD='cvs'; ############################################################################# # scvs flags. # This will be set to 1 if submit requested. Have to be left intact. my $SCVS_SUBMITTING = 0; # Specifies the default 'commit' behavior. This default corresponds to the # cvs's one. my $SCVS_RECURSIVE_SUBMIT = 1; # List of files to submit. Have to be left intact. my @SCVS_SUBMIT_LIST = (); # Default message for submit. my $SCVS_MESSAGE = ''; # Default editor for submit. my $SCVS_EDITOR = $ENV{ EDITOR} || 'vi'; ############################################################################# # & parse_repository ({{rep}}) # . extracts the method, user, host, port and directory from {{rep}}. # . There are four possibilities: # - /path/to/repository # - :method:/path/to/repository # - :user@hostname:/path/to/repository # - :method:user@hostname:/path/to/repository # # . This function is far from perfect and will produce strange results with # non-standard repositories. Has only been tested for :pserver: method. # ############################################################################# # & parse_cmdline () # . Parses the command line, extracts significant options, recognizes the # 'submit' command. sub parse_cmdline { my $i; my $rep; my @SCVS_ARGV = @ARGV; # Parsing global options first. for ( $i = 0; $i < @ARGV; $i++) { last unless $ARGV[ $i] =~ /^\-/; if ( $ARGV[ $i] eq '-d') { if ( $ARGV[ $i + 1] =~ /^\:/) { $rep=$ARGV[ $i + 1]; } # Removing -d option for the command line. splice @ARGV, $i, 2; redo; } if ( $ARGV[ $i] eq '-e') { $SCVS_EDITOR = $ARGV[ $i + 1]; splice @ARGV, $i, 2; redo; } } # Global options are over. Checking out the command. if ( ( $i < @ARGV) && $ARGV[ $i] eq 'submit') { # Submit requested, special considerations required. $ARGV[ $i++] = 'commit'; $SCVS_SUBMITTING = 1; for ( ; $i < @ARGV; $i++) { last unless $ARGV[ $i] =~ /^\-/; if ( $ARGV[ $i] eq '-F') { local $/; open MSG_FILE, "<$ARGV[ $i + 1]" or die "Cannot read $ARGV[ $i + 1]: $!"; $SCVS_MESSAGE = ; close MSG_FILE; splice @ARGV, $i, 2; redo; } if ( $ARGV[ $i] eq '-m') { $SCVS_MESSAGE = $ARGV[ $i + 1]; splice @ARGV, $i, 2; redo; } if ( $ARGV[ $i] =~ /R/) { $SCVS_RECURSIVE_SUBMIT = 1; } if ( $ARGV[ $i] =~ /l/) { $SCVS_RECURSIVE_SUBMIT = 0; } } # Now we must remember which files to submit if there are any. @SCVS_SUBMIT_LIST = splice @ARGV, $i; } @ARGV = @SCVS_ARGV unless $SCVS_SUBMITTING; } ############################################################################# # main # Parse the command line and extract the info we are interested in. parse_cmdline; # submit is considered OK if we aren't supposed to do it. my $submit_ok = ! $SCVS_SUBMITTING; # this will be unlinked later. my $tmpname; if ( $SCVS_SUBMITTING) { my ( $cvs_update_cmd, @update_output, @commit_msg); # get the list of interesting (added, modified, removed) files first. $cvs_update_cmd = "$CVS_CMD -q -n update " . ( $SCVS_RECURSIVE_SUBMIT ? '-R ' : '-l ') . join( ' ', @SCVS_SUBMIT_LIST); print "cvs_submit: Obtaining list of changes. This may take a while, please, wait...\n"; @update_output = grep { /^[ARM] /} split /\n/, `$cvs_update_cmd`; # submit failed if cvs returned non-zero exit code. $submit_ok = $? == 0; if ( $submit_ok && @update_output == 0) { print STDERR "cvs_submit: No files found to submit.\n"; $submit_ok = 0; } if ( $submit_ok) { # create a temporary file for editing. my $n = 0; # presume failure unless the temporary file will be eventually created. $submit_ok = 0; TMPFILE: while ( ! $submit_ok) { $tmpname = sprintf "/tmp/tmp%04d.txt", $n++; next if -f $tmpname; $submit_ok = sysopen MSG_FILE, $tmpname, O_CREAT | O_EXCL | O_WRONLY, 0600; unless ( $submit_ok) { if ( ! $!{ EEXIST}) { print STDERR "cvs_submit: Temporary file '$tmpname' creation failed: $!\n"; undef $tmpname; last TMPFILE; } } }; } if ( $submit_ok) { # succeed in creating temporary file, filling it with the form. print MSG_FILE $SCVS_MESSAGE, "\n"; # if a message template exists - add it just after the first empty line. # NOTE! Never use \t in the template because it will confuse scvs. if ( -f 'CVS/Template') { if ( open MSG_TEMPLATE, "; close MSG_TEMPLATE; } else { print STDERR "cvs_submit: Cannot read message template: $!\n"; } } # creating actual list of files to submit. my %sources = ( A => [ "CVS: Added:\n"], M => [ "CVS: Modified:\n"], R => [ "CVS: Removed:\n"], ); foreach my $src ( @update_output) { $src =~ s/^([ARM]) //; # \t used to distinguish lines with filenames from mere informative ones. push @{ $sources{ $1}}, "CVS:\t$src\n"; } # and forming the rest of the file. print MSG_FILE "CVS: ----------------------------------------------------------------------\n" . "CVS: Remove lines with filenames you don't want to commit.\n" . "CVS: Remember not to put TAB just after 'CVS:' or the rest of the line\n" . "CVS: will be treated as a filename to commit.\n" . "CVS: \n"; print MSG_FILE @{ $sources{ A}} > 1 ? @{ $sources{ A}} : '', @{ $sources{ M}} > 1 ? @{ $sources{ M}} : '', @{ $sources{ R}} > 1 ? @{ $sources{ R}} : ''; print MSG_FILE "CVS: ----------------------------------------------------------------------\n"; close MSG_FILE; # let the user edit the message. $submit_ok = system( $SCVS_EDITOR, $tmpname) == 0; } if ( $submit_ok) { $submit_ok = open MSG_FILE, "<$tmpname"; if ( ! $submit_ok) { print STDERR "cvs_submit: Can't read `$tmpname': $!\n"; } } if ( $submit_ok) { my $i; # read the message file back and split it into a message and CVS data. # Lines not beginning with 'CVS:\s' are treated as belonging to the message. # All lines after the first line wich begins with this string a treated as CVS # data. @commit_msg = map { chomp; $_} ; close MSG_FILE; my $msg_empty = 1; for ( $i = 0; $i < @commit_msg; $i++) { last if $commit_msg[ $i] =~ /^CVS\:\s/; if ( $commit_msg[ $i] !~ /^\s*$/) { $msg_empty = 0; } } # cut of the message. my @scvs_data = splice @commit_msg, $i; $submit_ok = ! $msg_empty; if ( ! $msg_empty) { $SCVS_MESSAGE = join( "\n", @commit_msg) . "\n"; } else { print STDERR "cvs_submit: Empty message, doing nothing.\n"; } # message file is broken if any of the CVS data lines doesn't start with # 'CVS:\s' or isn't just 'CVS:'. unless ( scalar( grep { ! /^CVS\:(?:$|\s+)/} @scvs_data) == 0) { $submit_ok = 0; print STDERR "cvs_submit: Message structure is wrong!\n"; } else { # filter out all informative lines and leave only filenames. @SCVS_SUBMIT_LIST = map { s/^CVS\:\s+//; $_} grep { /^CVS:\t/} splice @scvs_data, 1, scalar( @scvs_data) - 2; } } if ( $submit_ok) { # put the message and files back into the arguments. open MSG_FILE, ">$tmpname"; print MSG_FILE $SCVS_MESSAGE; close MSG_FILE; push @ARGV, '-F', $tmpname; push @ARGV, @SCVS_SUBMIT_LIST; } } if ( $submit_ok) { # Now we can call system to execute the cvs command. my $exitcode = system $CVS_CMD, @ARGV; if ($exitcode) { print STDERR "cvs_submit: CVS command has done with rc==$exitcode\n"; } } if ( $tmpname) { # clean up. unlink $tmpname; }