# ------------------------------------------------------------------------------ # (C) British Crown Copyright 2006-17 Met Office. # # This file is part of FCM, tools for managing and building source code. # # FCM 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 3 of the License, or # (at your option) any later version. # # FCM is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with FCM. If not, see . # ------------------------------------------------------------------------------ use strict; use warnings; # Utility to manipulate FCM commit messages. package FCM::System::CM::CommitMessage; use base qw{FCM::Class::CODE}; use Cwd qw{cwd}; use FCM::Context::Event; use FCM::System::Exception; use File::Spec::Functions qw{catfile}; use File::Temp; use Text::ParseWords qw{shellwords}; my $CTX = 'FCM::System::CM::CommitMessage::State'; my $E = 'FCM::System::Exception'; our $COMMIT_MESSAGE_BASE = '#commit_message#'; our $DELIMITER_USER = '--Add your commit message ABOVE - do not alter this line or those below--' . "\n"; our $DELIMITER_AUTO = '--FCM message (will be inserted automatically)--' . "\n"; our $DELIMITER_INFO = '--Change summary (not part of commit message)--' . "\n"; our $EDITOR = 'vi'; our $GEDITOR = 'gvim -f'; our $SUBVERSION_CONFIG_FILE = catfile((getpwuid($<))[7], qw{.subversion/config}); __PACKAGE__->class({gui => '$', util => '&'}, {action_of => { 'ctx' => sub {$CTX->new()}, 'edit' => \&_edit, 'load' => \&_load, 'notify' => \&_notify, 'path' => \&_path, 'path_base' => sub {$COMMIT_MESSAGE_BASE}, 'save' => \&_save, 'temp' => \&_temp, }}, ); # Invokes an editor to edit the commit message context. sub _edit { my ($attrib_ref, $commit_message_ctx) = @_; my $UTIL = $attrib_ref->{'util'}; my $temp = File::Temp->new(); if ($commit_message_ctx->get_user_part()) { print($temp $commit_message_ctx->get_user_part()); } else { print($temp "\n"); } print($temp $DELIMITER_USER); if ($commit_message_ctx->get_auto_part()) { print($temp $DELIMITER_AUTO . $commit_message_ctx->get_auto_part()); } print($temp $DELIMITER_INFO . $commit_message_ctx->get_info_part()); close($temp) || die("$temp: $!\n"); my $config_value; my $editor_command = $ENV{'SVN_EDITOR'} ? $ENV{'SVN_EDITOR'} : ($config_value = _svn_config_get($attrib_ref, 'helpers', 'editor-cmd')) ? $config_value : $ENV{'VISUAL'} ? $ENV{'VISUAL'} : $ENV{'EDITOR'} ? $ENV{'EDITOR'} : $attrib_ref->{gui} ? $GEDITOR : $EDITOR ; $UTIL->event(FCM::Context::Event->CM_LOG_EDIT, $editor_command); my @command = (shellwords($editor_command), $temp->filename()); !system(@command) || return $E->throw($E->SHELL, {command_list => \@command, rc => $?}); # Note: cannot use FCM::Util->shell method for terminal based editor. #my %value_of = %{$attrib_ref->{'util'}->shell_simple(\@command)}; #if ($value_of{'rc'}) { # return $E->throw($E->SHELL, {command_list => \@command, %value_of}); #} my $user_part = _parse( $attrib_ref, scalar($UTIL->file_load($temp->filename())), $DELIMITER_USER, ); $commit_message_ctx->set_user_part($user_part); if (($user_part . $commit_message_ctx->get_auto_part()) =~ qr{\A\s*\z}msx) { return $E->throw($E->CM_LOG_EDIT_NULL); } } # Reads a commit message file from $path or the standard location. Returns a # commit message context object. sub _load { my ($attrib_ref, $path) = @_; $path ||= _path($attrib_ref); my ($user_part, $auto_part) = eval { _parse($attrib_ref, scalar($attrib_ref->{'util'}->file_load($path))); }; if (my $e = $@) { $user_part = q{}; $auto_part = q{}; $@ = undef; # TODO: should raise a high verbosity event? } $CTX->new({'user_part' => $user_part, 'auto_part' => $auto_part}); } # Raises an CM_COMMIT_MESSAGE event for the commit message. sub _notify { my ($attrib_ref, $commit_message_ctx) = @_; $attrib_ref->{util}->event( FCM::Context::Event->CM_COMMIT_MESSAGE, $commit_message_ctx, ); } # Parses a commit message into the user and auto parts. Returns the user part in # scalar context. Returns (user_part, auto_part) in list context. sub _parse { my ($attrib_ref, $message, $no_delimiter_user) = @_; my @parts = (q{}, q{}); my $state = 0; LINE: for my $line (split("\n", $message)) { if ($state && !wantarray()) { last LINE; } $line .= "\n"; if ($line eq $DELIMITER_INFO) { last LINE; } elsif ($line eq $DELIMITER_AUTO) { $state = 1; next LINE; } elsif ($line eq $DELIMITER_USER) { $no_delimiter_user = undef; $state = -1; next LINE; } if ($state >= 0) { $parts[$state] .= $line; } } if ($no_delimiter_user) { return $E->throw($E->CM_LOG_EDIT_DELIMITER, $DELIMITER_USER); } for my $part (@parts) { $part =~ s{\A\s*(.*?)\s*\z}{$1}msx; if ($part) { $part .= "\n"; } } wantarray() ? @parts : $parts[0]; } # Returns the path to the commit message file in the current working directory # or the commit message file in $dir if $dir is set. sub _path { my ($attrib_ref, $dir) = @_; catfile(($dir ? $dir : cwd()), $COMMIT_MESSAGE_BASE); } # Saves the commit message to $path or the standard location for later # retrieval. sub _save { my ($attrib_ref, $commit_message_ctx, $path) = @_; $path ||= _path($attrib_ref); my $string = $commit_message_ctx->get_user_part(); if ($commit_message_ctx->get_auto_part()) { $string .= $DELIMITER_AUTO . $commit_message_ctx->get_auto_part(); } $attrib_ref->{'util'}->file_save($path, $string); } # Returns a File::Temp object containing a commit message ready for the VCS. sub _temp { my ($attrib_ref, $commit_message_ctx) = @_; my $temp = File::Temp->new(); print($temp $commit_message_ctx->get_user_part()); print($temp $commit_message_ctx->get_auto_part()); close($temp) || die("$temp: $!\n"); $temp; } # Loads a setting from $HOME/.subversion/config, and returns its value. sub _svn_config_get { my ($attrib_ref, $section, $key) = @_; # Note: can use Config::IniFiles, but best to avoid another dependency. # Note: not very efficient logic here, but should not yet matter. my $handle = $attrib_ref->{'util'}->file_load_handle($SUBVERSION_CONFIG_FILE); my $is_in_section; my $value; LINE: while (my $line = readline($handle)) { chomp($line); if ($line =~ qr{\A\s*(?:[#;]|\z)}msx) { next LINE; } if ($line =~ qr{\A\s*\[\s*$section\s*\]\s*\z}msx) { $is_in_section = 1; } elsif ($line =~ qr{\A\s*\[}msx) { $is_in_section = 0; } elsif ($is_in_section) { my ($rhs) = $line =~ qr{\A\s*$key\s*=\s*(.*)\z}msx; if (defined($rhs)) { $value = $rhs; } } } close($handle); $value; } #------------------------------------------------------------------------------- package FCM::System::CM::CommitMessage::State; use base qw{FCM::Class::HASH}; __PACKAGE__->class({ (map {($_ . '_part' => {isa => '$', default => q{}})} qw{auto info user}), }); #------------------------------------------------------------------------------- 1; __END__ =head1 NAME FCM::System::CM::CommitMessage =head1 SYNOPSIS use FCM::System::CM::CommitMessage; my $commit_message_util = FCM::System::CM::CommitMessage->new(\%attrib); my $commit_message_ctx = $commit_message_util->ctx(); $commit_message_util->edit($ctx); =head1 DESCRIPTION The commit message dumper, editor, loader, parser, etc for the FCM code management sub-system. =head1 METHODS =over 4 =item $class->new(\%attrib) Return a new instance. This class should normally be initialised by L. =item $commit_message_util->ctx() Return a new and empty commit message context. =item $commit_message_util->edit($commit_message_ctx) Invoke an editor to edit the commit message context. =item $commit_message_util->load($path) Load the content of a commit message file in $path, and return the result in a new commit message context. =item $commit_message_util->notify($commit_message_ctx) Raise a CM_COMMIT_MESSAGE event with the $commit_message_ctx. =item $commit_message_util->path($dir) Return the path to the commit message file in $dir or the current working directory if $dir is not specified. =item $commit_message_util->path($dir) Return the base name of the commit message file. =item $commit_message_util->save($commit_message_ctx, $path) Save the commit message to $path (or the standard location if $path is not specified). =item $commit_message_util->temp() Return a File::Temp object containing a commit message ready for the VCS. =back =head1 COPYRIGHT (C) Crown copyright Met Office. All rights reserved. =cut