#!/usr/bin/perl # ------------------------------------------------------------------------------ # NAME # fcm # # SYNOPSIS # fcm SUBCOMMAND [OPTIONS...] ARGS... # # DESCRIPTION # The fcm command is the frontend of the FCM system. The first argument to the # command must be a recognised subcommand. See "fcm help" for a full list of # functionalities. # # COPYRIGHT # (C) Crown copyright Met Office. All rights reserved. # For further details please refer to the file COPYRIGHT.txt # which you should have received as part of this distribution. # ------------------------------------------------------------------------------ # Standard pragmas: use warnings; use strict; # Standard modules: use File::Basename; use File::Spec; use Getopt::Long; use Cwd; # FCM component modules: use lib File::Spec->catfile (dirname (dirname ($0)), 'lib'); use Fcm::Config; use Fcm::Extract; use Fcm::Build; use Fcm::Util; BEGIN { eval { require Fcm::Cm; import Fcm::Cm; require Fcm::CmUrl; import Fcm::CmUrl; } } # Function declaration: sub cmp_ext_cfg; sub invoke_build_system; sub invoke_extract_system; sub invoke_cfg_printer; sub invoke_cm_system; sub invoke_www_browser; sub invoke_help; # ------------------------------------------------------------------------------ my $prog = basename $0; my $year = (localtime)[5] + 1900; my $copyright = < [qw/help ? h/], BLD => [qw/build bld/], EXT => [qw/extract ext/], CFG => [qw/cfg/], GUI => [qw/gui/], CM => [qw/ branch br conflicts cf add blame praise annotate ann cat checkout co cleanup commit ci copy cp delete del remove rm diff di export import info list ls lock log merge mkdir mkpatch move mv rename ren propdel pdel pd propedit pedit pe propget pget pg proplist plist pl propset pset ps resolved revert status stat st switch sw unlock update up /], CMP => [qw/cmp-ext-cfg/], WWW => [qw/www trac/], ); # Get configuration settings my $config = Fcm::Config->new (); $config->get_config (); # Determine the functionality of this invocation of the command my $function = @ARGV ? shift @ARGV : ''; # Run command accordingly if (grep {$_ eq $function} @{ $subcommand{BLD} }) { invoke_build_system; } elsif (grep {$_ eq $function} @{ $subcommand{EXT} }) { invoke_extract_system; } elsif (grep {$_ eq $function} @{ $subcommand{CFG} }) { invoke_cfg_printer; } elsif (grep {$_ eq $function} @{ $subcommand{GUI} }) { &run_command (['fcm_gui', @ARGV], METHOD => 'exec'); } elsif (grep {$_ eq $function} @{ $subcommand{CM} }) { invoke_cm_system; } elsif (grep {$_ eq $function} @{ $subcommand{CMP} }) { cmp_ext_cfg; } elsif (grep {$_ eq $function} @{ $subcommand{WWW} }) { invoke_www_browser; } elsif ($function =~ /^\s*$/ or grep {$_ eq $function} @{ $subcommand{HLP} }) { invoke_help; } else { w_report 'Unknown command: ', $function; e_report 'Type "', $prog, ' help" for usage'; } exit; # ------------------------------------------------------------------------------ # SYNOPSIS # $cfg = &main::cfg (); # # DESCRIPTION # Return the $config variable. # ------------------------------------------------------------------------------ sub cfg { return $config; } # ------------------------------------------------------------------------------ # SYNOPSIS # &cmp_ext_cfg (); # # DESCRIPTION # Compare two similar extract configuration files. # ------------------------------------------------------------------------------ sub cmp_ext_cfg { # Check options # ---------------------------------------------------------------------------- my ($wiki, $verbose); GetOptions ('wiki|w=s' => \$wiki, 'verbose|v' => \$verbose); # Check arguments # ---------------------------------------------------------------------------- e_report $prog, ' ', $function, ': 2 extract config files must be specified, abort.' if @ARGV < 2; # Invoke 2 new instances of the Fcm::Extract class # ---------------------------------------------------------------------------- my (@cfg, $rc); for my $i (0 .. 1) { $cfg[$i] = Fcm::Extract->new (CFG_SRC => $ARGV[$i]); # Read the extract configuration file $rc = $cfg[$i]->decipher_cfg; $rc = $cfg[$i]->expand_cfg if $rc; last if not $rc; } # Throw error if command has failed # ---------------------------------------------------------------------------- e_report $prog, ' ', $function, ': cannot read extract configuration file, abort' if not $rc; # Get list of URLs # ---------------------------------------------------------------------------- my @urls = (); for my $i (0 .. 1) { # List of branches in each extract configuration file my @branches = $cfg[$i]->branches; for my $branch (@branches) { # Ignore declarations of local directories next if $branch->type eq 'user'; # List of SRC declarations in each branch my %dirs = $branch->dirs; for my $dir (values %dirs) { # Set up a new instance of Fcm::CmUrl object for each SRC declaration my $cm_url = Fcm::CmUrl->new ( URL => $dir . ($branch->version ? '@' . $branch->version : ''), ); $urls[$i]{$cm_url->branch_url}{$dir} = $cm_url; } } } # Compare # ---------------------------------------------------------------------------- my %log; for my $i (0 .. 1) { # Compare the first file with the second one and then vice versa my $j = ($i == 0) ? 1 : 0; for my $branch (sort keys %{ $urls[$i] }) { if (exists $urls[$j]{$branch}) { # Same REPOS declarations in both files for my $dir (sort keys %{ $urls[$i]{$branch} }) { if (exists $urls[$j]{$branch}{$dir}) { # Same SRC declarations in both files, only need to compare once next if $i == 1; my $this_url = $urls[$i]{$branch}{$dir}; my $that_url = $urls[$j]{$branch}{$dir}; # Check whether their last changed revisions are the same my $this_rev = $this_url->svninfo (FLAG => 'Last Changed Rev'); my $that_rev = $that_url->svninfo (FLAG => 'Last Changed Rev'); # Make sure last changed revisions differ next if $this_rev eq $that_rev; # Not interested in the log before the minimum revision my $min_rev = ($this_url->pegrev > $that_url->pegrev) ? $that_url->pegrev : $this_url->pegrev; $this_rev = $min_rev if $this_rev < $min_rev; $that_rev = $min_rev if $that_rev < $min_rev; # Get list of changed revisions using the commit log my $u = ($this_rev > $that_rev) ? $this_url : $that_url; my %revs = $u->svnlog (REV => [$this_rev, $that_rev]); for my $rev (keys %revs) { # Check if revision is already in the list next if exists $log{$branch}{$rev}; # Not interested in the minimum revision next if $rev == $min_rev; # Get list of changed paths. Accept this revision only if it # contains changes in the current branch my %paths = %{ $revs{$rev}{paths} }; for my $path (keys %paths) { my $change_url = Fcm::CmUrl->new (URL => $u->root . $path); if ($change_url->branch eq $u->branch) { $log{$branch}{$rev} = $u; last; } } } } else { # Report SRC declaration in one file but not in another print $urls[$i]{$branch}{$dir}->url_peg, ':', "\n"; print ' in : ', $ARGV[$i], "\n"; print ' not in: ', $ARGV[$j], "\n\n"; } } } else { # Report REPOS declaration in one file but not in another print $branch, ':', "\n"; print ' in : ', $ARGV[$i], "\n"; print ' not in: ', $ARGV[$j], "\n\n"; } } } # Report modifications # ---------------------------------------------------------------------------- print 'Revisions at which declared source directories are modified:', "\n\n" if keys %log; if (defined $wiki) { # Output in wiki format my $wiki_url = Fcm::CmUrl->new (URL => &expand_url_keyword (URL => $wiki)); my $base_trac = $wiki ? &get_browser_url (URL => $wiki_url->project_url) : $wiki_url; $base_trac = $wiki_url if not $base_trac; for my $branch (sort keys %log) { # Name of the branch my $branch_trac = &get_browser_url (URL => $branch); $branch_trac =~ s#^$base_trac(?:/*|$)#source:#; print '[', $branch_trac, ']:', "\n"; # Revision table for my $rev (sort {$b <=> $a} keys %{ $log{$branch} }) { print $log{$branch}{$rev}->display_svnlog ($rev, $base_trac), "\n"; } print "\n"; } } else { my $separator = '-' x 80 . "\n"; for my $branch (sort keys %log) { # Output in plain text format print $branch, ':', "\n"; if ($verbose or &cfg->verbose > 1) { # Verbose mode, print revision log for my $rev (sort {$b <=> $a} keys %{ $log{$branch} }) { print $separator, $log{$branch}{$rev}->display_svnlog ($rev), "\n"; } } else { # Normal mode, print list of revisions print join (' ', sort {$b <=> $a} keys %{ $log{$branch} }), "\n"; } print $separator, "\n"; } } return $rc; } # ------------------------------------------------------------------------------ # SYNOPSIS # &invoke_build_system (); # # DESCRIPTION # Invoke the build system. # ------------------------------------------------------------------------------ sub invoke_build_system { my ($archive, $full, $ignore_lock, $jobs, $stage, @targets, $verbose); GetOptions ( 'archive|a' => \$archive, # switch on archive mode? 'full|f' => \$full, # full build? 'ignore-lock' => \$ignore_lock, # ignore lock file? 'jobs|j=i' => \$jobs, # number of parallel jobs in make 'stage|s=s' => \$stage, # build up to and including this stage 'targets|t=s' => \@targets, # make targets 'verbose|v=i' => \$verbose, # verbose level ); # Verbose level $config->verbose ($verbose) if defined $verbose; # Invoke a new instance of the Fcm::Build class my $bld = Fcm::Build->new (CFG_SRC => @ARGV ? join (' ', @ARGV) : cwd ()); # Perform build $bld->build ( ARCHIVE => $archive, FULL => $full, IGNORE_LOCK => $ignore_lock, JOBS => $jobs ? $jobs : 1, STAGE => $stage ? $stage : 5, TARGETS => (@targets ? [split (/:/, join (':', @targets))] : [qw/all/]), ); return 1; } # ------------------------------------------------------------------------------ # SYNOPSIS # &invoke_extract_system (); # # DESCRIPTION # Invoke the extract system. # ------------------------------------------------------------------------------ sub invoke_extract_system { my ($full, $ignore_lock, $verbose); GetOptions ( 'full|f' => \$full, # full extract? 'ignore-lock' => \$ignore_lock, # ignore lock file? 'verbose|v=i' => \$verbose, # verbose level ); $config->verbose ($verbose) if defined $verbose; # Invoke a new instance of the Fcm::Extract class my $ext = Fcm::Extract->new (CFG_SRC => @ARGV ? join (' ', @ARGV) : cwd ()); # Perform extract $ext->extract (FULL => $full, IGNORE_LOCK => $ignore_lock); return 1; } # ------------------------------------------------------------------------------ # SYNOPSIS # &invoke_cfg_printer (); # # DESCRIPTION # Invoke the CFG file pretty printer. # ------------------------------------------------------------------------------ sub invoke_cfg_printer { use Fcm::CfgFile; my $out_file; GetOptions ( 'output|o=s' => \$out_file, # output file for print ); my $file = join (' ', @ARGV); e_report $prog, ' ', $function, ': file not specified, abort.' if ! $file; # Invoke a new Fcm::CfgFile instance my $cfg = Fcm::CfgFile->new (SRC => $file); # Read the cfg file my $read = $cfg->read_cfg; e_report if not $read; # Pretty print CFG file $cfg->print_cfg ($out_file); return 1; } # ------------------------------------------------------------------------------ # SYNOPSIS # &invoke_cm_system (); # # DESCRIPTION # Invoke a code management system command. # ------------------------------------------------------------------------------ sub invoke_cm_system { &cm_command ($function); return 1; } # ------------------------------------------------------------------------------ # SYNOPSIS # &invoke_www_browser (); # # DESCRIPTION # Invoke a web browser on the specified PATH. # ------------------------------------------------------------------------------ sub invoke_www_browser { # Options my ($browser); GetOptions ( 'browser|b=s' => \$browser, # browser command ); $browser = &cfg->setting (qw/MISC WEB_BROWSER/) unless $browser; # Arguments my ($arg) = @ARGV ? $ARGV[0] : (&is_wc () ? '.' : ''); e_report $prog, ' ', $function, ': input URL not specified and . not a working copy, abort.' if not $arg; # Local PATH? $arg = &expand_tilde ($arg); $arg = &get_url_of_wc ($arg) if -e $arg; # Expand URL and revision keywords my $www_url = &expand_url_keyword (URL => $arg); my $rev = 'HEAD'; if ($www_url =~ m#^(\w+://\S+)@(\S+)$#) { $www_url = $1; $rev = $2; } $rev = &expand_rev_keyword (URL => $www_url, REV => $rev, HEAD => 1) unless uc ($rev) eq 'HEAD'; # Get web browser URL $www_url = &get_browser_url (URL => $www_url); die 'WWW URL not defined for "', $arg, '", abort' unless $www_url; $www_url = $www_url . '?rev=' . $rev; # Execute command my @command = (split (/\s+/, $browser), $www_url); &run_command (\@command, METHOD => 'exec', PRINT => 1); } # ------------------------------------------------------------------------------ # SYNOPSIS # &invoke_help (); # # DESCRIPTION # Invoke help. # ------------------------------------------------------------------------------ sub invoke_help { my $cmd = @ARGV ? shift @ARGV : undef; if ($cmd) { if (grep {$_ eq $cmd} @{ $subcommand{BLD} }) { print < 1); } else { warn $prog, ' help: "', $cmd, '" not recognised'; $cmd = undef; } } if (not $cmd) { # Get output from "svn help" my @lines = &run_command ( [qw/svn help/], DEVNULL => 1, METHOD => 'qx', ERROR => 'ignore', ); # Get release number, (and revision number from revision number file) my $release = &cfg->setting ('RELEASE'); my $rev_file = &cfg->setting ('REV_FILE'); if (-r $rev_file) { open FILE, '<', $rev_file; my $rev = readline 'FILE'; close FILE; chomp $rev; $release .= '-dev (r' . $rev . ')' if $rev; } # Print common help print < [options] [args] Flexible configuration management system, release $release. Type "$prog help " for help on a specific subcommand. Available subcommands: help (h, ?) - help build (bld) - build system EOF # The following are only available on platforms with "svn" installed if (@lines) { print < - any Subversion sub-commands EOF } # Print FCM copyright notice print $copyright; # Print output from "svn help" if (@lines) { print "\n"; &print_command ([qw/svn help/]); print @lines; } } return 1; } # ------------------------------------------------------------------------------ # SYNOPSIS # $ans = &main::get_input (MESSAGE => $mesg, TYPE => $type, DEFAULT => $def); # # DESCRIPTION # Get an input string from the user and return it as $ans. MESSAGE is the # main message printed on screen to prompt the user for an input. If TYPE is # 'YN', print message to prompt user to enter either 'y' or 'n'. If TYPE is # 'YNA', then 'a' is given as a third option. If DEFAULT is set, print message # to inform user that the return value will be set to the $def (if nothing is # entered). # ------------------------------------------------------------------------------ sub get_input { my %args = @_; my $type = exists $args{TYPE} ? $args{TYPE} : ''; my $mesg = exists $args{MESSAGE} ? $args{MESSAGE} : ''; my $def = exists $args{DEFAULT} ? $args{DEFAULT} : ''; my $ans; while (1) { # Print the prompt print $mesg; print "\n", 'Enter "y" or "n"' if uc ($type) eq 'YN'; print "\n", 'Enter "y", "n" or "a"' if uc ($type) eq 'YNA'; print ' (or just press for "', $def, '")' if $def; print ': '; # Get answer from STDIN $ans = ; chomp $ans; # Set answer to default, if necessary $ans = $def if ($def and not $ans); if ($type =~ /^yna?$/i) { # For YN and YNA type dialog boxes, # check that the answer is in the correct form my $pat = (uc ($type) eq 'YN' ? 'y|n' : 'y|n|a'); last if $ans =~ /^(?:$pat)/i; } else { last; } } return $ans; } # ------------------------------------------------------------------------------ __END__