ReposBranch.pm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. # ------------------------------------------------------------------------------
  2. # NAME
  3. # Fcm::ReposBranch
  4. #
  5. # DESCRIPTION
  6. # This class contains methods for gathering information for a repository
  7. # branch. It currently supports Subversion repository and local user
  8. # directory.
  9. #
  10. # COPYRIGHT
  11. # (C) Crown copyright Met Office. All rights reserved.
  12. # For further details please refer to the file COPYRIGHT.txt
  13. # which you should have received as part of this distribution.
  14. # ------------------------------------------------------------------------------
  15. use warnings;
  16. use strict;
  17. package Fcm::ReposBranch;
  18. use base qw{Fcm::Base};
  19. use Fcm::CfgLine;
  20. use Fcm::Keyword;
  21. use Fcm::Util qw{expand_tilde is_url run_command w_report};
  22. use File::Basename qw{dirname};
  23. use File::Find qw{find};
  24. use File::Spec;
  25. # List of scalar property methods for this class
  26. my @scalar_properties = (
  27. 'package', # package name of which this repository belongs
  28. 'repos', # repository branch root URL/path
  29. 'revision', # the revision of this branch
  30. 'tag', # "tag" name of this branch of the repository
  31. 'type', # repository type
  32. );
  33. # List of hash property methods for this class
  34. my @hash_properties = (
  35. 'dirs', # list of non-recursive directories in this branch
  36. 'expdirs', # list of recursive directories in this branch
  37. );
  38. # ------------------------------------------------------------------------------
  39. # SYNOPSIS
  40. # $obj = Fcm::ReposBranch->new (%args);
  41. #
  42. # DESCRIPTION
  43. # This method constructs a new instance of the Fcm::ReposBranch class. See
  44. # @scalar_properties above for allowed list of properties in the constructor.
  45. # (KEYS should be in uppercase.)
  46. # ------------------------------------------------------------------------------
  47. sub new {
  48. my $this = shift;
  49. my %args = @_;
  50. my $class = ref $this || $this;
  51. my $self = Fcm::Base->new (%args);
  52. for (@scalar_properties) {
  53. $self->{$_} = exists $args{uc ($_)} ? $args{uc ($_)} : undef;
  54. }
  55. $self->{$_} = {} for (@hash_properties);
  56. bless $self, $class;
  57. return $self;
  58. }
  59. # ------------------------------------------------------------------------------
  60. # SYNOPSIS
  61. # $value = $obj->X;
  62. # $obj->X ($value);
  63. #
  64. # DESCRIPTION
  65. # Details of these properties are explained in @scalar_properties.
  66. # ------------------------------------------------------------------------------
  67. for my $name (@scalar_properties) {
  68. no strict 'refs';
  69. *$name = sub {
  70. my $self = shift;
  71. # Argument specified, set property to specified argument
  72. if (@_) {
  73. $self->{$name} = $_[0];
  74. }
  75. return $self->{$name};
  76. }
  77. }
  78. # ------------------------------------------------------------------------------
  79. # SYNOPSIS
  80. # %hash = %{ $obj->X () };
  81. # $obj->X (\%hash);
  82. #
  83. # $value = $obj->X ($index);
  84. # $obj->X ($index, $value);
  85. #
  86. # DESCRIPTION
  87. # Details of these properties are explained in @hash_properties.
  88. #
  89. # If no argument is set, this method returns a hash containing a list of
  90. # objects. If an argument is set and it is a reference to a hash, the objects
  91. # are replaced by the the specified hash.
  92. #
  93. # If a scalar argument is specified, this method returns a reference to an
  94. # object, if the indexed object exists or undef if the indexed object does
  95. # not exist. If a second argument is set, the $index element of the hash will
  96. # be set to the value of the argument.
  97. # ------------------------------------------------------------------------------
  98. for my $name (@hash_properties) {
  99. no strict 'refs';
  100. *$name = sub {
  101. my ($self, $arg1, $arg2) = @_;
  102. # Ensure property is defined as a reference to a hash
  103. $self->{$name} = {} if not defined ($self->{$name});
  104. # Argument 1 can be a reference to a hash or a scalar index
  105. my ($index, %hash);
  106. if (defined $arg1) {
  107. if (ref ($arg1) eq 'HASH') {
  108. %hash = %$arg1;
  109. } else {
  110. $index = $arg1;
  111. }
  112. }
  113. if (defined $index) {
  114. # A scalar index is defined, set and/or return the value of an element
  115. $self->{$name}{$index} = $arg2 if defined $arg2;
  116. return (
  117. exists $self->{$name}{$index} ? $self->{$name}{$index} : undef
  118. );
  119. } else {
  120. # A scalar index is not defined, set and/or return the hash
  121. $self->{$name} = \%hash if defined $arg1;
  122. return $self->{$name};
  123. }
  124. }
  125. }
  126. # ------------------------------------------------------------------------------
  127. # SYNOPSIS
  128. # $rc = $obj->expand_revision;
  129. #
  130. # DESCRIPTION
  131. # This method expands the revision keywords of the current branch to a
  132. # revision number. It returns true on success.
  133. # ------------------------------------------------------------------------------
  134. sub expand_revision {
  135. my $self = shift;
  136. my $rc = 1;
  137. if ($self->type eq 'svn') {
  138. # Expand revision keyword
  139. my $rev = (Fcm::Keyword::expand($self->repos(), $self->revision()))[1];
  140. # Get last changed revision of the specified revision
  141. my $info_ref = $self->_svn_info($self->repos(), $rev);
  142. if (!defined($info_ref->{'Revision'})) {
  143. my $url = $self->repos() . ($rev ? '@' . $rev : q{});
  144. w_report("ERROR: $url: not a valid URL\n");
  145. return 0;
  146. }
  147. my $lc_rev = $info_ref->{'Last Changed Rev'};
  148. $rev = $info_ref->{'Revision'};
  149. # Print info if specified revision is not the last commit revision
  150. if (uc($self->revision()) ne 'HEAD' && $lc_rev != $rev) {
  151. my $message = $self->repos . '@' . $rev . ': last changed at [' .
  152. $lc_rev . '].';
  153. if ($self->setting ('EXT_REVMATCH') and uc ($self->revision) ne 'HEAD') {
  154. w_report "ERROR: specified and last changed revisions differ:\n",
  155. ' ', $message, "\n";
  156. $rc = 0;
  157. } else {
  158. print 'INFO: ', $message, "\n";
  159. }
  160. }
  161. if ($self->verbose > 1 and uc ($self->revision) ne 'HEAD') {
  162. # See if there is a later change of the branch at the HEAD
  163. my $head_lc_rev = $self->_svn_info($self->repos())->{'Last Changed Rev'};
  164. if (defined($head_lc_rev) && $head_lc_rev != $lc_rev) {
  165. # Ensure that this is the same branch by checking its history
  166. my @lines = &run_command (
  167. [qw/svn log -q --incremental -r/, $lc_rev, $self->repos . '@HEAD'],
  168. METHOD => 'qx', TIME => $self->verbose > 2,
  169. );
  170. print 'INFO: ', $self->repos, '@', $rev,
  171. ': newest commit at [', $head_lc_rev, '].', "\n"
  172. if @lines;
  173. }
  174. }
  175. $self->revision ($rev) if $rev ne $self->revision;
  176. } elsif ($self->type eq 'user') {
  177. 1; # Do nothing
  178. } else {
  179. w_report 'ERROR: ', $self->repos, ': repository type "', $self->type,
  180. '" not supported.';
  181. $rc = 0;
  182. }
  183. return $rc;
  184. }
  185. # ------------------------------------------------------------------------------
  186. # SYNOPSIS
  187. # $rc = $obj->expand_path;
  188. #
  189. # DESCRIPTION
  190. # This method expands the relative path names of sub-directories to full
  191. # path names. It returns true on success.
  192. # ------------------------------------------------------------------------------
  193. sub expand_path {
  194. my $self = shift;
  195. my $rc = 1;
  196. if ($self->type eq 'svn') {
  197. # SVN repository
  198. # Do nothing unless there is a declared repository for this branch
  199. return unless $self->repos;
  200. # Remove trailing /
  201. my $repos = $self->repos;
  202. $self->repos ($repos) if $repos =~ s#/+$##;
  203. # Consider all declared (expandable) sub-directories
  204. for my $name (qw/dirs expdirs/) {
  205. for my $dir (keys %{ $self->$name }) {
  206. # Do nothing if declared sub-directory is quoted as a full URL
  207. next if &is_url ($self->$name ($dir));
  208. # Expand sub-directory to full URL
  209. $self->$name ($dir, $self->repos . (
  210. $self->$name ($dir) ? ('/' . $self->$name ($dir)) : ''
  211. ));
  212. }
  213. }
  214. # Note: "catfile" cannot be used in the above statement because it has
  215. # the tendency of removing a slash from double slashes.
  216. } elsif ($self->type eq 'user') {
  217. # Local user directories
  218. # Expand leading ~ for all declared (expandable) sub-directories
  219. for my $name (qw/dirs expdirs/) {
  220. for my $dir (keys %{ $self->$name }) {
  221. $self->$name ($dir, expand_tilde $self->$name ($dir));
  222. }
  223. }
  224. # A top directory for the source is declared
  225. if ($self->repos) {
  226. # Expand leading ~ for the top directory
  227. $self->repos (expand_tilde $self->repos);
  228. # Get the root directory of the file system
  229. my $rootdir = File::Spec->rootdir ();
  230. # Expand top directory to absolute path, if necessary
  231. $self->repos (File::Spec->rel2abs ($self->repos))
  232. if $self->repos !~ m/^$rootdir/;
  233. # Remove trailing /
  234. my $repos = $self->repos;
  235. $self->repos ($repos) if $repos =~ s#/+$##;
  236. # Consider all declared (expandable) sub-directories
  237. for my $name (qw/dirs expdirs/) {
  238. for my $dir (keys %{ $self->$name }) {
  239. # Do nothing if declared sub-directory is quoted as a full path
  240. next if $self->$name ($dir) =~ m#^$rootdir#;
  241. # Expand sub-directory to full path
  242. $self->$name (
  243. $dir, $self->$name ($dir)
  244. ? File::Spec->catfile ($self->repos, $self->$name ($dir))
  245. : $self->repos
  246. );
  247. }
  248. }
  249. }
  250. } else {
  251. w_report 'ERROR: ', $self->repos, ': repository type "', $self->type,
  252. '" not supported.';
  253. $rc = 0;
  254. }
  255. return $rc;
  256. }
  257. # ------------------------------------------------------------------------------
  258. # SYNOPSIS
  259. # $rc = $obj->expand_all();
  260. #
  261. # DESCRIPTION
  262. # This method searches the expandable source directories recursively for
  263. # source directories containing regular files. The namespaces and the locators
  264. # of these sub-directories are then added to the source directory hash table.
  265. # Returns true on success.
  266. # ------------------------------------------------------------------------------
  267. sub expand_all {
  268. my ($self) = @_;
  269. my %finder_of = (
  270. user => sub {
  271. my ($root_locator) = @_;
  272. my %ns_of;
  273. my $wanted = sub {
  274. my $base_name = $_;
  275. my $path = $File::Find::name;
  276. if (-f $path && -r $path && !-l $path) {
  277. my $dir_path = dirname($path);
  278. my $rel_dir_path = File::Spec->abs2rel($dir_path, $root_locator);
  279. if (!exists($ns_of{$dir_path})) {
  280. $ns_of{$dir_path} = [File::Spec->splitdir($rel_dir_path)];
  281. }
  282. }
  283. };
  284. find($wanted, $root_locator);
  285. return \%ns_of;
  286. },
  287. svn => sub {
  288. my ($root_locator) = @_;
  289. my $runner = sub {
  290. map {chomp($_); $_} run_command(
  291. ['svn', @_, '-R', join('@', $root_locator, $self->revision())],
  292. METHOD => 'qx', TIME => $self->config()->verbose() > 2,
  293. );
  294. };
  295. # FIXME: check for symlink switched off due to "svn pg" being very slow
  296. #my %symlink_in
  297. # = map {($_ =~ qr{\A(.+)\s-\s(\*)\z}xms)} ($runner->(qw{pg svn:special}));
  298. #my @locators
  299. # = grep {$_ !~ qr{/\z}xms && !$symlink_in{$_}} ($runner->('ls'));
  300. my @locators = grep {$_ !~ qr{/\z}xms} ($runner->('ls'));
  301. my %ns_of;
  302. for my $locator (@locators) {
  303. my ($rel_dir_locator) = $locator =~ qr{\A(.*)/[^/]+\z}xms; # dirname
  304. $rel_dir_locator ||= q{};
  305. my $dir_locator = join(q{/}, $root_locator, $rel_dir_locator);
  306. if (!exists($ns_of{$dir_locator})) {
  307. $ns_of{$dir_locator} = [split(q{/}, $rel_dir_locator)];
  308. }
  309. }
  310. return \%ns_of;
  311. },
  312. );
  313. if (!defined($finder_of{$self->type()})) {
  314. w_report(sprintf(
  315. qq{ERROR: %s: resource type "%s" not supported},
  316. $self->repos(),
  317. $self->type(),
  318. ));
  319. return;
  320. }
  321. while (my ($root_ns, $root_locator) = each(%{$self->expdirs()})) {
  322. my @root_ns_list = split(qr{$Fcm::Config::DELIMITER}xms, $root_ns);
  323. my $ns_hash_ref = $finder_of{$self->type()}->($root_locator);
  324. while (my ($dir_path, $ns_list_ref) = each(%{$ns_hash_ref})) {
  325. if (!grep {$_ =~ qr{\A\.}xms || $_ =~ qr{~\z}xms} @{$ns_list_ref}) {
  326. my $ns = join($Fcm::Config::DELIMITER, @root_ns_list, @{$ns_list_ref});
  327. $self->dirs($ns, $dir_path);
  328. }
  329. }
  330. }
  331. return 1;
  332. }
  333. # ------------------------------------------------------------------------------
  334. # SYNOPSIS
  335. # $n = $obj->add_base_dirs ($base);
  336. #
  337. # DESCRIPTION
  338. # Add a list of source directories to the current branch based on the set
  339. # provided by $base, which must be a reference to a Fcm::ReposBranch
  340. # instance. It returns the total number of used sub-directories in the
  341. # current repositories.
  342. # ------------------------------------------------------------------------------
  343. sub add_base_dirs {
  344. my $self = shift;
  345. my $base = shift;
  346. my %base_dirs = %{ $base->dirs };
  347. for my $key (keys %base_dirs) {
  348. # Remove repository root from base directories
  349. if ($base_dirs{$key} eq $base->repos) {
  350. $base_dirs{$key} = '';
  351. } else {
  352. $base_dirs{$key} = substr $base_dirs{$key}, length ($base->repos) + 1;
  353. }
  354. # Append base directories to current repository root
  355. $self->dirs ($key, $base_dirs{$key});
  356. }
  357. # Expand relative path names of sub-directories
  358. $self->expand_path;
  359. return scalar keys %{ $self->dirs };
  360. }
  361. # ------------------------------------------------------------------------------
  362. # SYNOPSIS
  363. # @cfglines = $obj->to_cfglines ();
  364. #
  365. # DESCRIPTION
  366. # This method returns a list of configuration lines for the current branch.
  367. # ------------------------------------------------------------------------------
  368. sub to_cfglines {
  369. my ($self) = @_;
  370. my @return = ();
  371. my $suffix = $self->package . $Fcm::Config::DELIMITER . $self->tag;
  372. push @return, Fcm::CfgLine->new (
  373. label => $self->cfglabel ('REPOS') . $Fcm::Config::DELIMITER . $suffix,
  374. value => $self->repos,
  375. ) if $self->repos;
  376. push @return, Fcm::CfgLine->new (
  377. label => $self->cfglabel ('REVISION') . $Fcm::Config::DELIMITER . $suffix,
  378. value => $self->revision,
  379. ) if $self->revision;
  380. for my $key (sort keys %{ $self->dirs }) {
  381. my $value = $self->dirs ($key);
  382. # Use relative path where possible
  383. if ($self->repos) {
  384. if ($value eq $self->repos) {
  385. $value = '';
  386. } elsif (index ($value, $self->repos) == 0) {
  387. $value = substr ($value, length ($self->repos) + 1);
  388. }
  389. }
  390. # Use top package name where possible
  391. my $dsuffix = $key . $Fcm::Config::DELIMITER . $self->tag;
  392. $dsuffix = $suffix if $value ne $self->dirs ($key) and $key eq join (
  393. $Fcm::Config::DELIMITER, $self->package, File::Spec->splitdir ($value)
  394. );
  395. push @return, Fcm::CfgLine->new (
  396. label => $self->cfglabel ('DIRS') . $Fcm::Config::DELIMITER . $dsuffix,
  397. value => $value,
  398. );
  399. }
  400. push @return, Fcm::CfgLine->new ();
  401. return @return;
  402. }
  403. # ------------------------------------------------------------------------------
  404. # SYNOPSIS
  405. # my $hash_ref = $self->_svn_info($url[, $rev]);
  406. #
  407. # DESCRIPTION
  408. # Executes "svn info" and returns each field in a hash.
  409. # ------------------------------------------------------------------------------
  410. sub _svn_info {
  411. my ($self, $url, $rev) = @_;
  412. return {
  413. map {
  414. chomp();
  415. my ($key, $value) = split(qr{\s*:\s*}xms, $_, 2);
  416. $key ? ($key, $value) : ();
  417. } run_command(
  418. [qw{svn info}, ($rev ? ('-r', $rev, join('@', $url, $rev)) : $url)],
  419. DEVNULL => 1, METHOD => 'qx', TIME => $self->verbose() > 2,
  420. )
  421. };
  422. }
  423. # ------------------------------------------------------------------------------
  424. 1;
  425. __END__