123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- # ------------------------------------------------------------------------------
- # NAME
- # Fcm::ReposBranch
- #
- # DESCRIPTION
- # This class contains methods for gathering information for a repository
- # branch. It currently supports Subversion repository and local user
- # directory.
- #
- # 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.
- # ------------------------------------------------------------------------------
- use warnings;
- use strict;
- package Fcm::ReposBranch;
- use base qw{Fcm::Base};
- use Fcm::CfgLine;
- use Fcm::Keyword;
- use Fcm::Util qw{expand_tilde is_url run_command w_report};
- use File::Basename qw{dirname};
- use File::Find qw{find};
- use File::Spec;
- # List of scalar property methods for this class
- my @scalar_properties = (
- 'package', # package name of which this repository belongs
- 'repos', # repository branch root URL/path
- 'revision', # the revision of this branch
- 'tag', # "tag" name of this branch of the repository
- 'type', # repository type
- );
- # List of hash property methods for this class
- my @hash_properties = (
- 'dirs', # list of non-recursive directories in this branch
- 'expdirs', # list of recursive directories in this branch
- );
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $obj = Fcm::ReposBranch->new (%args);
- #
- # DESCRIPTION
- # This method constructs a new instance of the Fcm::ReposBranch class. See
- # @scalar_properties above for allowed list of properties in the constructor.
- # (KEYS should be in uppercase.)
- # ------------------------------------------------------------------------------
- sub new {
- my $this = shift;
- my %args = @_;
- my $class = ref $this || $this;
- my $self = Fcm::Base->new (%args);
- for (@scalar_properties) {
- $self->{$_} = exists $args{uc ($_)} ? $args{uc ($_)} : undef;
- }
- $self->{$_} = {} for (@hash_properties);
- bless $self, $class;
- return $self;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $value = $obj->X;
- # $obj->X ($value);
- #
- # DESCRIPTION
- # Details of these properties are explained in @scalar_properties.
- # ------------------------------------------------------------------------------
- for my $name (@scalar_properties) {
- no strict 'refs';
- *$name = sub {
- my $self = shift;
- # Argument specified, set property to specified argument
- if (@_) {
- $self->{$name} = $_[0];
- }
- return $self->{$name};
- }
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # %hash = %{ $obj->X () };
- # $obj->X (\%hash);
- #
- # $value = $obj->X ($index);
- # $obj->X ($index, $value);
- #
- # DESCRIPTION
- # Details of these properties are explained in @hash_properties.
- #
- # If no argument is set, this method returns a hash containing a list of
- # objects. If an argument is set and it is a reference to a hash, the objects
- # are replaced by the the specified hash.
- #
- # If a scalar argument is specified, this method returns a reference to an
- # object, if the indexed object exists or undef if the indexed object does
- # not exist. If a second argument is set, the $index element of the hash will
- # be set to the value of the argument.
- # ------------------------------------------------------------------------------
- for my $name (@hash_properties) {
- no strict 'refs';
- *$name = sub {
- my ($self, $arg1, $arg2) = @_;
- # Ensure property is defined as a reference to a hash
- $self->{$name} = {} if not defined ($self->{$name});
- # Argument 1 can be a reference to a hash or a scalar index
- my ($index, %hash);
- if (defined $arg1) {
- if (ref ($arg1) eq 'HASH') {
- %hash = %$arg1;
- } else {
- $index = $arg1;
- }
- }
- if (defined $index) {
- # A scalar index is defined, set and/or return the value of an element
- $self->{$name}{$index} = $arg2 if defined $arg2;
- return (
- exists $self->{$name}{$index} ? $self->{$name}{$index} : undef
- );
- } else {
- # A scalar index is not defined, set and/or return the hash
- $self->{$name} = \%hash if defined $arg1;
- return $self->{$name};
- }
- }
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $rc = $obj->expand_revision;
- #
- # DESCRIPTION
- # This method expands the revision keywords of the current branch to a
- # revision number. It returns true on success.
- # ------------------------------------------------------------------------------
- sub expand_revision {
- my $self = shift;
- my $rc = 1;
- if ($self->type eq 'svn') {
- # Expand revision keyword
- my $rev = (Fcm::Keyword::expand($self->repos(), $self->revision()))[1];
- # Get last changed revision of the specified revision
- my $info_ref = $self->_svn_info($self->repos(), $rev);
- if (!defined($info_ref->{'Revision'})) {
- my $url = $self->repos() . ($rev ? '@' . $rev : q{});
- w_report("ERROR: $url: not a valid URL\n");
- return 0;
- }
- my $lc_rev = $info_ref->{'Last Changed Rev'};
- $rev = $info_ref->{'Revision'};
- # Print info if specified revision is not the last commit revision
- if (uc($self->revision()) ne 'HEAD' && $lc_rev != $rev) {
- my $message = $self->repos . '@' . $rev . ': last changed at [' .
- $lc_rev . '].';
- if ($self->setting ('EXT_REVMATCH') and uc ($self->revision) ne 'HEAD') {
- w_report "ERROR: specified and last changed revisions differ:\n",
- ' ', $message, "\n";
- $rc = 0;
- } else {
- print 'INFO: ', $message, "\n";
- }
- }
- if ($self->verbose > 1 and uc ($self->revision) ne 'HEAD') {
- # See if there is a later change of the branch at the HEAD
- my $head_lc_rev = $self->_svn_info($self->repos())->{'Last Changed Rev'};
- if (defined($head_lc_rev) && $head_lc_rev != $lc_rev) {
- # Ensure that this is the same branch by checking its history
- my @lines = &run_command (
- [qw/svn log -q --incremental -r/, $lc_rev, $self->repos . '@HEAD'],
- METHOD => 'qx', TIME => $self->verbose > 2,
- );
- print 'INFO: ', $self->repos, '@', $rev,
- ': newest commit at [', $head_lc_rev, '].', "\n"
- if @lines;
- }
- }
- $self->revision ($rev) if $rev ne $self->revision;
- } elsif ($self->type eq 'user') {
- 1; # Do nothing
- } else {
- w_report 'ERROR: ', $self->repos, ': repository type "', $self->type,
- '" not supported.';
- $rc = 0;
- }
- return $rc;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $rc = $obj->expand_path;
- #
- # DESCRIPTION
- # This method expands the relative path names of sub-directories to full
- # path names. It returns true on success.
- # ------------------------------------------------------------------------------
- sub expand_path {
- my $self = shift;
- my $rc = 1;
- if ($self->type eq 'svn') {
- # SVN repository
- # Do nothing unless there is a declared repository for this branch
- return unless $self->repos;
- # Remove trailing /
- my $repos = $self->repos;
- $self->repos ($repos) if $repos =~ s#/+$##;
- # Consider all declared (expandable) sub-directories
- for my $name (qw/dirs expdirs/) {
- for my $dir (keys %{ $self->$name }) {
- # Do nothing if declared sub-directory is quoted as a full URL
- next if &is_url ($self->$name ($dir));
- # Expand sub-directory to full URL
- $self->$name ($dir, $self->repos . (
- $self->$name ($dir) ? ('/' . $self->$name ($dir)) : ''
- ));
- }
- }
- # Note: "catfile" cannot be used in the above statement because it has
- # the tendency of removing a slash from double slashes.
- } elsif ($self->type eq 'user') {
- # Local user directories
- # Expand leading ~ for all declared (expandable) sub-directories
- for my $name (qw/dirs expdirs/) {
- for my $dir (keys %{ $self->$name }) {
- $self->$name ($dir, expand_tilde $self->$name ($dir));
- }
- }
- # A top directory for the source is declared
- if ($self->repos) {
- # Expand leading ~ for the top directory
- $self->repos (expand_tilde $self->repos);
- # Get the root directory of the file system
- my $rootdir = File::Spec->rootdir ();
- # Expand top directory to absolute path, if necessary
- $self->repos (File::Spec->rel2abs ($self->repos))
- if $self->repos !~ m/^$rootdir/;
- # Remove trailing /
- my $repos = $self->repos;
- $self->repos ($repos) if $repos =~ s#/+$##;
- # Consider all declared (expandable) sub-directories
- for my $name (qw/dirs expdirs/) {
- for my $dir (keys %{ $self->$name }) {
- # Do nothing if declared sub-directory is quoted as a full path
- next if $self->$name ($dir) =~ m#^$rootdir#;
- # Expand sub-directory to full path
- $self->$name (
- $dir, $self->$name ($dir)
- ? File::Spec->catfile ($self->repos, $self->$name ($dir))
- : $self->repos
- );
- }
- }
- }
- } else {
- w_report 'ERROR: ', $self->repos, ': repository type "', $self->type,
- '" not supported.';
- $rc = 0;
- }
- return $rc;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $rc = $obj->expand_all();
- #
- # DESCRIPTION
- # This method searches the expandable source directories recursively for
- # source directories containing regular files. The namespaces and the locators
- # of these sub-directories are then added to the source directory hash table.
- # Returns true on success.
- # ------------------------------------------------------------------------------
- sub expand_all {
- my ($self) = @_;
- my %finder_of = (
- user => sub {
- my ($root_locator) = @_;
- my %ns_of;
- my $wanted = sub {
- my $base_name = $_;
- my $path = $File::Find::name;
- if (-f $path && -r $path && !-l $path) {
- my $dir_path = dirname($path);
- my $rel_dir_path = File::Spec->abs2rel($dir_path, $root_locator);
- if (!exists($ns_of{$dir_path})) {
- $ns_of{$dir_path} = [File::Spec->splitdir($rel_dir_path)];
- }
- }
- };
- find($wanted, $root_locator);
- return \%ns_of;
- },
- svn => sub {
- my ($root_locator) = @_;
- my $runner = sub {
- map {chomp($_); $_} run_command(
- ['svn', @_, '-R', join('@', $root_locator, $self->revision())],
- METHOD => 'qx', TIME => $self->config()->verbose() > 2,
- );
- };
- # FIXME: check for symlink switched off due to "svn pg" being very slow
- #my %symlink_in
- # = map {($_ =~ qr{\A(.+)\s-\s(\*)\z}xms)} ($runner->(qw{pg svn:special}));
- #my @locators
- # = grep {$_ !~ qr{/\z}xms && !$symlink_in{$_}} ($runner->('ls'));
- my @locators = grep {$_ !~ qr{/\z}xms} ($runner->('ls'));
- my %ns_of;
- for my $locator (@locators) {
- my ($rel_dir_locator) = $locator =~ qr{\A(.*)/[^/]+\z}xms; # dirname
- $rel_dir_locator ||= q{};
- my $dir_locator = join(q{/}, $root_locator, $rel_dir_locator);
- if (!exists($ns_of{$dir_locator})) {
- $ns_of{$dir_locator} = [split(q{/}, $rel_dir_locator)];
- }
- }
- return \%ns_of;
- },
- );
- if (!defined($finder_of{$self->type()})) {
- w_report(sprintf(
- qq{ERROR: %s: resource type "%s" not supported},
- $self->repos(),
- $self->type(),
- ));
- return;
- }
- while (my ($root_ns, $root_locator) = each(%{$self->expdirs()})) {
- my @root_ns_list = split(qr{$Fcm::Config::DELIMITER}xms, $root_ns);
- my $ns_hash_ref = $finder_of{$self->type()}->($root_locator);
- while (my ($dir_path, $ns_list_ref) = each(%{$ns_hash_ref})) {
- if (!grep {$_ =~ qr{\A\.}xms || $_ =~ qr{~\z}xms} @{$ns_list_ref}) {
- my $ns = join($Fcm::Config::DELIMITER, @root_ns_list, @{$ns_list_ref});
- $self->dirs($ns, $dir_path);
- }
- }
- }
- return 1;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # $n = $obj->add_base_dirs ($base);
- #
- # DESCRIPTION
- # Add a list of source directories to the current branch based on the set
- # provided by $base, which must be a reference to a Fcm::ReposBranch
- # instance. It returns the total number of used sub-directories in the
- # current repositories.
- # ------------------------------------------------------------------------------
- sub add_base_dirs {
- my $self = shift;
- my $base = shift;
- my %base_dirs = %{ $base->dirs };
- for my $key (keys %base_dirs) {
- # Remove repository root from base directories
- if ($base_dirs{$key} eq $base->repos) {
- $base_dirs{$key} = '';
- } else {
- $base_dirs{$key} = substr $base_dirs{$key}, length ($base->repos) + 1;
- }
- # Append base directories to current repository root
- $self->dirs ($key, $base_dirs{$key});
- }
- # Expand relative path names of sub-directories
- $self->expand_path;
- return scalar keys %{ $self->dirs };
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # @cfglines = $obj->to_cfglines ();
- #
- # DESCRIPTION
- # This method returns a list of configuration lines for the current branch.
- # ------------------------------------------------------------------------------
- sub to_cfglines {
- my ($self) = @_;
- my @return = ();
- my $suffix = $self->package . $Fcm::Config::DELIMITER . $self->tag;
- push @return, Fcm::CfgLine->new (
- label => $self->cfglabel ('REPOS') . $Fcm::Config::DELIMITER . $suffix,
- value => $self->repos,
- ) if $self->repos;
- push @return, Fcm::CfgLine->new (
- label => $self->cfglabel ('REVISION') . $Fcm::Config::DELIMITER . $suffix,
- value => $self->revision,
- ) if $self->revision;
- for my $key (sort keys %{ $self->dirs }) {
- my $value = $self->dirs ($key);
- # Use relative path where possible
- if ($self->repos) {
- if ($value eq $self->repos) {
- $value = '';
- } elsif (index ($value, $self->repos) == 0) {
- $value = substr ($value, length ($self->repos) + 1);
- }
- }
- # Use top package name where possible
- my $dsuffix = $key . $Fcm::Config::DELIMITER . $self->tag;
- $dsuffix = $suffix if $value ne $self->dirs ($key) and $key eq join (
- $Fcm::Config::DELIMITER, $self->package, File::Spec->splitdir ($value)
- );
- push @return, Fcm::CfgLine->new (
- label => $self->cfglabel ('DIRS') . $Fcm::Config::DELIMITER . $dsuffix,
- value => $value,
- );
- }
- push @return, Fcm::CfgLine->new ();
- return @return;
- }
- # ------------------------------------------------------------------------------
- # SYNOPSIS
- # my $hash_ref = $self->_svn_info($url[, $rev]);
- #
- # DESCRIPTION
- # Executes "svn info" and returns each field in a hash.
- # ------------------------------------------------------------------------------
- sub _svn_info {
- my ($self, $url, $rev) = @_;
- return {
- map {
- chomp();
- my ($key, $value) = split(qr{\s*:\s*}xms, $_, 2);
- $key ? ($key, $value) : ();
- } run_command(
- [qw{svn info}, ($rev ? ('-r', $rev, join('@', $url, $rev)) : $url)],
- DEVNULL => 1, METHOD => 'qx', TIME => $self->verbose() > 2,
- )
- };
- }
- # ------------------------------------------------------------------------------
- 1;
- __END__
|