fcm_update_version_dir.pl 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. #!/usr/bin/env perl
  2. #-------------------------------------------------------------------------------
  3. # (C) Crown copyright Met Office. All rights reserved.
  4. # For further details please refer to the file COPYRIGHT.txt
  5. # which you should have received as part of this distribution.
  6. #-------------------------------------------------------------------------------
  7. use strict;
  8. use warnings;
  9. use FindBin;
  10. use lib "$FindBin::Bin/../lib";
  11. use Cwd qw{cwd};
  12. use Getopt::Long qw{GetOptions};
  13. use Fcm::Config;
  14. use Fcm::Keyword;
  15. use Fcm::Util qw{get_url_of_wc get_wct is_wc run_command tidy_url};
  16. use File::Basename qw{basename dirname};
  17. use File::Path qw{mkpath};
  18. use File::Spec;
  19. use Pod::Usage qw{pod2usage};
  20. # Usage
  21. # ------------------------------------------------------------------------------
  22. my $this = basename($0);
  23. # Options
  24. # ------------------------------------------------------------------------------
  25. my ($dest, $full, $help, $url);
  26. my $rc = GetOptions(
  27. 'dest|d=s' => \$dest,
  28. 'full|f' => \$full,
  29. 'help' => \$help,
  30. 'url|u=s' => \$url,
  31. );
  32. if (!$rc) {
  33. pod2usage({'-verbose' => 1});
  34. }
  35. if ($help) {
  36. pod2usage({'-exitval' => 0, '-verbose' => 1});
  37. }
  38. if (!$url) {
  39. pod2usage(
  40. {'-message' => 'The --url=URL option is compulsory', '-verbose' => 1},
  41. );
  42. }
  43. $dest ||= cwd();
  44. # Arguments
  45. # ------------------------------------------------------------------------------
  46. if (@ARGV) {
  47. die 'Cannot read: ', $ARGV[0], ', abort' unless -f $ARGV[0] and -r $ARGV[0];
  48. }
  49. # Get configuration settings
  50. # ------------------------------------------------------------------------------
  51. my $config = Fcm::Config->new ();
  52. $config->get_config ();
  53. # Expand URL keyword
  54. $url = Fcm::Util::tidy_url(Fcm::Keyword::expand($url));
  55. # ------------------------------------------------------------------------------
  56. MAIN: {
  57. my $date = localtime;
  58. print $this, ': started on ', $date, "\n";
  59. my %dirs;
  60. # Read input (file) for a list directories and update conditions
  61. while (<>) {
  62. chomp;
  63. # Ignore empty and comment lines
  64. next if /^\s*(?:#|$)/;
  65. # Each line must contain a relative path, and optionally a list of
  66. # space delimited conditions
  67. my @words = split /\s+/;
  68. my $dir = shift @words;
  69. # Check that the conditions are valid
  70. my @conditions;
  71. for my $word (@words) {
  72. if ($word =~ /^([<>]=?|[!=]=)(.+)$/i) {
  73. # Condition must be a conditional operator followed by a revision
  74. my ($operator, $rev) = ($1, $2);
  75. $rev = (Fcm::Keyword::expand($url, $rev))[1];
  76. push @conditions, $operator . $rev;
  77. } else {
  78. print STDERR 'Warning: ignore unknown syntax for update condition: ',
  79. $word, "\n";
  80. }
  81. }
  82. # Add directory and its conditions to a hash
  83. if ($dir =~ s#/\*$##) { # Directory finishes with wildcard
  84. # Run "svn ls" in recursive mode
  85. my $dirurl = join ('/', ($url, $dir));
  86. my @files = &run_command ([qw/svn ls -R/, $dirurl], METHOD => 'qx');
  87. # Find directories containing regular files
  88. while (my $file = shift @files) {
  89. # Skip directories
  90. next if $file =~ m#/$#;
  91. # Get "dirname" of regular file and add to hash
  92. my $subdir = join ('/', ($dir, dirname ($file)));
  93. $dirs{$subdir} = \@conditions;
  94. }
  95. } else {
  96. $dirs{$dir} = \@conditions;
  97. }
  98. }
  99. # Update each directory, if required
  100. for my $dir (sort keys %dirs) {
  101. # Use "svn log" to determine the revisions that need to be updated
  102. my %allversions;
  103. {
  104. my $command = 'svn log -q ' . join ('/', ($url, $dir));
  105. my @log = &run_command (
  106. [qw/svn log -q/, join ('/', ($url, $dir))], METHOD => 'qx',
  107. );
  108. @log = grep /^r\d+/, @log;
  109. # Assign a sequential "version" number to each sub-directory
  110. my $version = scalar @log;
  111. for (@log) {
  112. m/^r(\d+)/;
  113. $allversions{$1} = 'v' . $version--;
  114. }
  115. }
  116. my %versions = %allversions;
  117. # Extract only revisions matching the conditions
  118. if (@{ $dirs{$dir} }) {
  119. my @conditions = @{ $dirs{$dir} };
  120. for my $condition (@conditions) {
  121. for my $rev (keys %versions) {
  122. delete $versions{$rev} unless eval ($rev . $condition);
  123. }
  124. }
  125. }
  126. # Destination directory
  127. my $dirpath = File::Spec->catfile ($dest, $dir);
  128. if (-d $dirpath) {
  129. if ($full or not keys %versions) {
  130. # Remove destination directory top, in full mode
  131. # or if there are no matching revisions
  132. &run_command ([qw/rm -rf/, $dirpath], PRINT => 1);
  133. } else {
  134. # Delete excluded revisions if they exist, in incremental mode
  135. if (opendir DIR, $dirpath) {
  136. while (my $rev = readdir 'DIR') {
  137. next unless $rev =~ /^\d+$/;
  138. if (not grep {$_ eq $rev} keys %versions) {
  139. my @command = (qw/rm -rf/, File::Spec->catfile ($dirpath, $rev));
  140. &run_command (\@command, PRINT => 1);
  141. # Remove "version" symlink
  142. my $verlink = File::Spec->catfile ($dirpath, $allversions{$rev});
  143. unlink $verlink if -l $verlink;
  144. }
  145. }
  146. closedir DIR;
  147. }
  148. }
  149. }
  150. # Create container directory of destination if it does not already exist
  151. if (keys %versions and not -d $dirpath) {
  152. print '-> mkdir -p ', $dirpath, "\n";
  153. my $rc = mkpath $dirpath;
  154. die 'mkdir -p ', $dirpath, ' failed' unless $rc;
  155. }
  156. # Update each version directory that needs updating
  157. for my $rev (keys %versions) {
  158. my $revpath = File::Spec->catfile ($dest, $dir, $rev);
  159. # Create version directory if it does not exist
  160. if (not -e $revpath) {
  161. # Use "svn export" to create the version directory
  162. my @command = (
  163. qw/svn export -q -r/,
  164. $rev,
  165. join ('/', ($url, $dir)),
  166. $revpath,
  167. );
  168. &run_command (\@command, PRINT => 1);
  169. }
  170. # Create "version" symlink if necessary
  171. my $verlink = File::Spec->catfile ($dest, $dir, $versions{$rev});
  172. symlink $rev, $verlink unless -l $verlink;
  173. }
  174. # Symbolic link to the "latest" version directory
  175. my $headlink = File::Spec->catfile ($dest, $dir, 'latest');
  176. my $headrev = 0;
  177. for my $rev (keys %versions) {
  178. $headrev = $rev if $rev > $headrev;
  179. }
  180. if (-l $headlink) {
  181. # Remove old symbolic link if there is no revision to update or if it
  182. # does not point to the correct version directory
  183. my $org = readlink $headlink;
  184. unlink $headlink if (! $headrev or $org ne $headrev);
  185. }
  186. # (Re-)create the "latest" symbolic link, if necessary
  187. symlink $headrev, $headlink if ($headrev and not -l $headlink);
  188. }
  189. $date = localtime;
  190. print $this, ': finished normally on ', $date, "\n";
  191. }
  192. __END__
  193. =head1 NAME
  194. fcm_update_version_dir.pl
  195. =head1 SYNOPSIS
  196. fcm_update_version_dir.pl [OPTIONS] [CFGFILE]
  197. =head1 DESCRIPTION
  198. Update the version directories for a list of relative paths in the source
  199. repository URL.
  200. =head1 OPTIONS
  201. =over 4
  202. =item --dest=DEST, -d DEST
  203. Specify a destination for the extraction. If not specified, the command extracts
  204. to the current working directory.
  205. =item --help, -h
  206. Print help and exit.
  207. =item --full, -f
  208. Specify the full mode. If not specified, the command runs in incremental mode.
  209. =item --url=URL, -u URL
  210. Specify the source repository URL. No default.
  211. =back
  212. =head1 ARGUMENTS
  213. A configuration file may be given to this command, or it will attempt to read
  214. from the standard input. Each line in the configuration must contain a relative
  215. path that resides under the given source repository URL. (Empty lines and lines
  216. beginning with a "#" are ignored.) Optionally, each relative path may be
  217. followed by a list of space separated "conditions". Each condition is a
  218. conditional operator (>, >=, <, <=, == or !=) followed by a revision number or
  219. the keyword HEAD. The command uses the revision log to determine the revisions
  220. at which the relative path has been updated in the source repository URL. If
  221. these revisions also satisfy the "conditions" set by the user, they will be
  222. considered in the extraction. In full mode, everything is re-extracted. In
  223. incremental mode, the version directories are only updated if they do not
  224. already exist.
  225. =head1 COPYRIGHT
  226. (C) Crown copyright Met Office. All rights reserved.
  227. =cut