#!/usr/bin/env perl # # svnci.pl -- Utility script that removes files that are deleted from # the log message from the commit. # # Copyright 2006 Troy Curtis Jr. # # 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 of the License, or (at your # option) any later version. # # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN # NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # 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. # #------------------------------------------------------------------------------ # $Id: svnci.pl 24 2006-09-20 04:33:48Z troycurtisjr $ # $HeadURL: https://troyjr.hopto.org/projects/svn/svnci/tags/0.1/svnci.pl $ # You can get the latest file anonymously by using http:// in place of https:// #------------------------------------------------------------------------------ # This script is designed to function as a drop-in replacement for using # 'svn commit'. It parses all of the command-line arguments that the 1.4 # version of svn takes. # # Ultimately I strived to make svnci.pl behave the same way 'svn commit' would # with the notable exception that you can delete files from the log message # to remove them from the commit. Additionally, if you have a blank log # message this script will only ask you to abort or try again. Implementing a # blank commit option seemed like it was going to be more trouble than it was # worth. Besides, you shouldn't use blank log messages! Please let me know of # any deviations from normal 'svn commit' behavior that bug you. # # If you supply a message, log message filename, or targets file on the command # line, this script will simply pass the arguments directly to 'svn commit'. # This is done because being able to delete files # from your commit does not # make sense in these cases. # # Feel free to send me patches or suggestions. #------------------------------------------------------------------------------ use strict; #------------------------------------------------------------------------------ # User Configuration Section #------------------------------------------------------------------------------ # The name of the svn executable you want to use with this script my $svn_exec = 'svn'; # The editor to use if checks to SVN_EDITOR, VISUAL, and EDITOR fail. my $default_editor = 'vim'; # The base and extension of the temporary file to save the log message to. It # defaults to the svn-commit[#].tmp filename that the svn client uses. my $tmpfile_base = 'svn-commit'; my $tmpfile_ext = '.tmp'; # The name of the temporary targets file to use when providing 'svn commit' # with a list of files to commit. I did this so that I would not have to fight # the shell with special characters. my $targets_name = 'svnci-targets.tmp'; # The *single* line that you want to seperate your log message from your # filelist. I have included a commented out version of the 'svn' seperator for # convience (syntax files, etc). Of course, the real seperator isn't exactly # true anymore if you use this script. :-) my $log_seperator = "--Type your log message above this line. You may delete files from the commit below.--"; #my $log_seperator = "--This line, and those below, will be ignored--"; #------------------------------------------------------------------------------ # Packages #------------------------------------------------------------------------------ use Getopt::Long; #------------------------------------------------------------------------------ # Globals #------------------------------------------------------------------------------ my @old_commit_logs = (); my %args = ('verbose' => 0); # Initialize verbose to zero my %commit_candidates = (); # Option strings to pass to the Getopts::Long module. # See the format_option subroutine for why we need to define these two arrays. # Boolean options. Either true or false. my @bool_options = ( 'quiet|q', 'no-unlock', 'no-auth-cache', 'non-interactive', 'non-recursive|N'); # Options that may have arguments associated with them. my @arg_options = ( 'message|m=s', 'file|F=s', 'force-log', 'editor-cmd=s', 'encoding=s', 'username=s', 'password=s', 'targets=s', 'config-dir=s'); # Options that should be passed through to the status command my @status_valid_options = ( 'quiet|q', 'non-recursive|N', 'username=s', 'password=s', 'no-auth-cache', 'non-interactive', 'config-dir=s'); my $status_options = ''; # Command line options to use when calling the 'svn status' command my $commit_options = ''; # Command line options to use when calling the 'svn commit' command my $file_list = ''; # All the other args on the command line my $log_file = ''; #------------------------------------------------------------------------------ # Function Definitions #------------------------------------------------------------------------------ ############################################################################### # trace(level,message) # # - level: The verbosity level of the supplied message # - message: A string containing the message to (maybe) display to the user # # Returns: Nothing # # Used anytime we want to say something to the user, allows for varying levels # of verbosity. Mostly useful for debugging/development, but there is no # reason to take it out. ############################################################################### sub trace { die "trace only accepts two options!\n" unless (scalar @_ == 2) ; (my $level, my $message) = @_; print $message unless $level > $args{'verbose'}; } ############################################################################### # getTmpFile() # # Returns: Filename of the temporary log message file # # Finds the first available filename using the $tmpfile_base[#].$tmpfile_ext # format (just like subversion). ############################################################################### sub getTmpFile { my $num = ""; my $tmp_filename = ''; # Loop through possible names of the tmp file until a free one is found while ( -f ($tmp_filename = $tmpfile_base . $num . $tmpfile_ext) ) { push @old_commit_logs, $tmp_filename; # If the $num variable is null, then we need to start the count, otherwise just increment by one if ( $num == '' ) { $num = 1 } else { $num++ } } return $tmp_filename; } ############################################################################### # generateLogTemplate(log_filename,commitlist) # # - log_filename: The filename to store the log message in # - commitlist: A string containing the list of files available for commit # # Returns: Nothing # # Generates the log message template that is given to the user's editor. This # consists of inserting the $log_seperator followed by the commitlist. ############################################################################### sub generateLogTemplate { (my $log_filename, my $commitlist) = @_; #print "log_filename: $log_filename\ncommitlist: $commitlist\n"; if ( $commitlist eq "" ) { die "You must have at least one file or directory in the commitlist\n"; } open LOG_FILE,">$log_filename" or die "Cannot open $log_filename for writing: $!\n"; print LOG_FILE "\n"; # Print a new line to make it easier to start a log message print LOG_FILE $log_seperator . "\n\n"; print LOG_FILE $commitlist . "\n"; close LOG_FILE or die "Error closing $log_filename: $!\n"; } ############################################################################### # format_option(option,value) # # - option: Name of the option to format # - value: Value of the option to format # # Returns: The formatted option string # # Generates a formatted option string that can be passed to the 'svn' CLI # client. This is needed because the Getopt::Long module gives a 0/1 value for # boolean options. ############################################################################### sub format_option { die "format_option must have 2 arguments!\n" unless scalar @_ == 2; (my $option, my $value) = @_; # Check to see if this is expected to be a bool option foreach (@bool_options) { if (/($option)/) { trace 5, "$option was found in the bool option array\n"; return " --$option "; } } if ( $value eq '' ) { trace 5, "$option has a NULL value\n"; return " --$option "; } trace 5, "$option has a value \"$value\"\n"; return " --$option=\"$value\" "; } ############################################################################### # callExternalEditor(log_filename) # # - log_filename: The filename of the log message to give to the editor. # # Returns: Exist status of the system call that executed the editor command. # # Opens the given log file with an editor. The CLI specified 'editor-cmd' # takes priority, followed by SVN_EDITOR, VISUAL, EDITOR, and finally # $default_editor (just like svn...mostly). ############################################################################### sub callExternalEditor { if ( exists ($args{'editor-cmd'})) { trace 3, "Using the command line supplied editor: $args{'editor-cmd'}\n"; return(system($args{'editor-cmd'},@_)); } elsif ( exists ($ENV{'SVN_EDITOR'}) && ($ENV{'SVN_EDITOR'} != '')) { trace 3, "Using the editor in SVN_EDITOR: $ENV{'SVN_EDITOR'}\n"; return(system($ENV{'SVN_EDITOR'},@_)); } elsif ( exists ($ENV{'VISUAL'}) && ($ENV{'VISUAL'} != '')) { trace 3, "Using the editor in VISUAL: $ENV{'VISUAL'}\n"; return(system($ENV{'VISUAL'},@_)); } elsif ( exists ($ENV{'EDITOR'}) && ($ENV{'EDITOR'} != '')) { trace 3, "Using the editor in EDITOR: $ENV{'EDITOR'}\n"; return(system($ENV{'EDITOR'},@_)); } else { trace 3, "Using the default editor $default_editor\n"; return(system($default_editor,@_)); } } ############################################################################### # parseCmdLine() # # Returns: Nothing # # Parse all of the command line parameters. We strip out the added 'verbose' # option, as that is only used by svnci.pl. We also build the option strings to # pass to both the 'svn status' (to get the commit candidates) and 'svn commit' # commands. ############################################################################### sub parseCmdLine { Getopt::Long::Configure("bundling"); GetOptions( \%args, 'verbose|v+', @bool_options, @arg_options ); while( (my $option, my $value) = each %args ) { next if $option =~ /verbose/; # We are using 'verbose' for this script so just skip it # Loop through all the valid status options, if we find a match # with the current option, then add it to the status option string foreach (@status_valid_options) { if ( /($option)/ ) { trace 4, "Adding $option/$value to status_options\n"; $status_options .= format_option($option,$value); } } $commit_options .= format_option($option,$value); trace 3,"$option specified with $value\n"; } # Now stick the rest of the args into the $file_list var $file_list = "@ARGV"; trace 2, "status_options: $status_options\n"; trace 2, "commit_options: $commit_options\n"; trace 2, "file_list : $file_list\n"; } ############################################################################### # parseLogFile(filename) # # - filename: Filename of the log file to parse # # Returns: Length of log message (i.e. 0 means an empty log message). # # This is the core function of this script! This is the function responsible # for parsing the user supplied log file and determining which file to commit # and with what log message. # # Logic: # * Test for an empty log message # * Leave the log message in the file # * Get the list of files that the user left (and thus wants to commit) # * Truncate the file at the $log_seperator line # * Delete any file paths from the global $commit_candidates hash if the user # deleted them from the log message. # * Return the length of the log message ############################################################################### sub parseLogFile { (my $filename) = @_; my $log_message = ''; my $log_filelist = ''; my $end_of_msg_addr; # Open the log file for reading open LOG_FILE,"+< $filename" or die "Unable to open $filename for update: $!\n"; # Save off the log message, which is all the text up to the $log_seperator while() { last if /^($log_seperator)$/; # Save the address of the last line of the log message $end_of_msg_addr = tell(LOG_FILE); $log_message .= $_; } trace 4, "The log message was:\n $log_message \n"; # We can continue processing the log message as long as there is at least # one non-whitespace character unless ( $log_message =~ /[^ \t\n]/s ) { close LOG_FILE; return 0; } # Grab all of the file paths that the user left while() { # Skip blank lines next if (/^[ \t]*$/); $log_filelist .= $_; } trace 4, "The user left this filelist:\n $log_filelist \n"; # Loop through all the commit_candidates that we created from the status message # and delete any from the hash that do not exist in the log_filelist foreach my $commit_file (keys %commit_candidates) { delete($commit_candidates{"$commit_file"}) unless ($log_filelist =~ /($commit_candidates{"$commit_file"})/); } trace 4, "We are left with these commit candidates:\n" . (keys %commit_candidates) . "\n"; # Truncate the file at the end of the log message truncate(LOG_FILE, $end_of_msg_addr); close LOG_FILE; return length($log_message); } ############################################################################### # cleanupFiles() # # Returns: Nothing # # Clean up any temporary files created by svnci.pl. ############################################################################### sub cleanupFiles { system('rm','-f',$log_file,$targets_name); } #------------------------------------------------------------------------------ # MAIN -- The entry point for this script, don't ask me why this is implemented # as a function which is simply called directly after its definition, I # do not know :-). #------------------------------------------------------------------------------ sub Main { my $status_output = ''; my $commit_list = ''; # Parse the command line arguments. Basically we will just # pass all of these on to 'svn commit' with the exception of # the -v/--verbose option which this script will use for it's # own purposes parseCmdLine(); # If the user specified the message, file, or targets parameter # then we can assume that he does not want our fancy functionality. if ( defined($args{'message'}) || defined($args{'file'}) || defined($args{'targets'})) { exec "$svn_exec commit $commit_options"; } $log_file = getTmpFile(); $status_output = `$svn_exec status $status_options $file_list`; foreach ( split /\n/, $status_output ) { my $file_path = $_; next if /^[?!]/; # Don't include the paths with ? or ! at the beginning $file_path =~ s/^.{7}//; $commit_candidates{"$file_path"} = $_; $commit_list .= $_ . "\n"; } if ( "$commit_list" eq '' ) { trace 0,"There is nothing to commit!\n"; exit 0; } generateLogTemplate($log_file,$commit_list); GET_LOG_MSG: { if(callExternalEditor($log_file)) { die "There was an error calling your external editor\n"; } if ( 0 == parseLogFile($log_file) ) { trace 0,"Your log message is empty, what would you like to do?\n"; trace 0," a)bort e)dit: "; my $ANS = ; if ($ANS =~ /^[ ]*[eE]/) { redo GET_LOG_MSG; } else { trace 0,"Not commiting any files!\n"; cleanupFiles(); exit 0; } } } if( (keys %commit_candidates) != () ) { # Create a targets file to pass to svn commit. This is better than including # all the paths in the system call because of special character escaping issues. open TARGETS_FILE,">$targets_name" or die "Could not open $targets_name for writing: $!\n"; foreach my $candidates (keys %commit_candidates) { print TARGETS_FILE "$candidates" . "\n"; } close TARGETS_FILE; my $svn_commit_command = "$svn_exec commit --targets $targets_name --file $log_file $commit_options"; # Alright, we finally have the command line parameters, our log message, and # the files we want to commit. Commit it already! trace 1,"Executing the following subversion command:\n$svn_commit_command\n"; print `$svn_commit_command`; } else { trace 0,"Nothing to commit!\n"; } cleanupFiles(); } Main(@_); #------------------------------------------------------------------------------ # POD Style documentation will eventually go here!! #------------------------------------------------------------------------------