ExtractFile.pm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. # ------------------------------------------------------------------------------
  2. # NAME
  3. # Fcm::ExtractFile
  4. #
  5. # DESCRIPTION
  6. # Select/combine a file in different branches and extract it to destination.
  7. #
  8. # COPYRIGHT
  9. # (C) Crown copyright Met Office. All rights reserved.
  10. # For further details please refer to the file COPYRIGHT.txt
  11. # which you should have received as part of this distribution.
  12. # ------------------------------------------------------------------------------
  13. use warnings;
  14. use strict;
  15. package Fcm::ExtractFile;
  16. use base qw{Fcm::Base};
  17. use Fcm::Util qw{run_command w_report};
  18. use File::Basename qw{dirname};
  19. use File::Compare qw{compare};
  20. use File::Copy qw{copy};
  21. use File::Path qw{mkpath};
  22. use File::Spec;
  23. use File::Temp qw(tempfile);
  24. # List of property methods for this class
  25. my @scalar_properties = (
  26. 'conflict', # conflict mode
  27. 'dest', # search path to destination file
  28. 'dest_status', # destination status, see below
  29. 'pkgname', # package name of this file
  30. 'src', # list of Fcm::ExtractSrc, specified for this file
  31. 'src_actual', # list of Fcm::ExtractSrc, actually used by this file
  32. 'src_status', # source status, see below
  33. );
  34. # Status code definition for $self->dest_status
  35. our %DEST_STATUS_CODE = (
  36. '' => 'unchanged',
  37. 'M' => 'modified',
  38. 'A' => 'added',
  39. 'a' => 'added, overridding inherited',
  40. 'D' => 'deleted',
  41. 'd' => 'deleted, overridding inherited',
  42. '?' => 'irrelevant',
  43. );
  44. # Status code definition for $self->src_status
  45. our %SRC_STATUS_CODE = (
  46. 'A' => 'added by a branch',
  47. 'B' => 'from the base',
  48. 'D' => 'deleted by a branch',
  49. 'M' => 'modified by a branch',
  50. 'G' => 'merged from 2+ branches',
  51. 'O' => 'overridden by a branch',
  52. '?' => 'irrelevant',
  53. );
  54. # ------------------------------------------------------------------------------
  55. # SYNOPSIS
  56. # $obj = Fcm::ExtractFile->new ();
  57. #
  58. # DESCRIPTION
  59. # This method constructs a new instance of the Fcm::ExtractFile class.
  60. # ------------------------------------------------------------------------------
  61. sub new {
  62. my $this = shift;
  63. my %args = @_;
  64. my $class = ref $this || $this;
  65. my $self = Fcm::Base->new (%args);
  66. for (@scalar_properties) {
  67. $self->{$_} = exists $args{$_} ? $args{$_} : undef;
  68. }
  69. bless $self, $class;
  70. return $self;
  71. }
  72. # ------------------------------------------------------------------------------
  73. # SYNOPSIS
  74. # $value = $obj->X;
  75. # $obj->X ($value);
  76. #
  77. # DESCRIPTION
  78. # Details of these properties are explained in @scalar_properties.
  79. # ------------------------------------------------------------------------------
  80. for my $name (@scalar_properties) {
  81. no strict 'refs';
  82. *$name = sub {
  83. my $self = shift;
  84. # Argument specified, set property to specified argument
  85. if (@_) {
  86. $self->{$name} = $_[0];
  87. }
  88. # Default value for property
  89. if (not defined $self->{$name}) {
  90. if ($name eq 'conflict') {
  91. $self->{$name} = 'merge'; # default to "merge" mode
  92. } elsif ($name eq 'dest' or $name eq 'src' or $name eq 'src_actual') {
  93. $self->{$name} = []; # default to an empty list
  94. }
  95. }
  96. return $self->{$name};
  97. }
  98. }
  99. # ------------------------------------------------------------------------------
  100. # SYNOPSIS
  101. # $rc = $obj->run();
  102. #
  103. # DESCRIPTION
  104. # This method runs only if $self->dest_status is not defined. It updates the
  105. # destination according to the source in the list and the conflict mode
  106. # setting. It updates the file in $self->dest as appropriate and sets
  107. # $self->dest_status. (See above.) This method returns true on success.
  108. # ------------------------------------------------------------------------------
  109. sub run {
  110. my ($self) = @_;
  111. my $rc = 1;
  112. if (not defined ($self->dest_status)) {
  113. # Assume file unchanged
  114. $self->dest_status ('');
  115. if (@{ $self->src }) {
  116. my $used;
  117. # Determine or set up a file for comparing with the destination
  118. ($rc, $used) = $self->run_get_used();
  119. # Attempt to compare the destination with $used. Update on change.
  120. if ($rc) {
  121. $rc = defined ($used) ? $self->run_update($used) : $self->run_delete();
  122. }
  123. } else {
  124. # No source, delete file in destination
  125. $self->src_status ('?');
  126. $rc = $self->run_delete();
  127. }
  128. }
  129. return $rc;
  130. }
  131. # ------------------------------------------------------------------------------
  132. # SYNOPSIS
  133. # $rc = $obj->run_delete();
  134. #
  135. # DESCRIPTION
  136. # This method is part of run(). It detects this file in the destination path.
  137. # If this file is in the current destination, it attempts to delete it and
  138. # sets the dest_status to "D". If this file is in an inherited destination,
  139. # it sets the dest_status to "d".
  140. # ------------------------------------------------------------------------------
  141. sub run_delete {
  142. my ($self) = @_;
  143. my $rc = 1;
  144. $self->dest_status ('?');
  145. for my $i (0 .. @{ $self->dest } - 1) {
  146. my $dest = File::Spec->catfile ($self->dest->[$i], $self->pkgname);
  147. next unless -f $dest;
  148. if ($i == 0) {
  149. $rc = unlink $dest;
  150. $self->dest_status ('D');
  151. } else {
  152. $self->dest_status ('d');
  153. last;
  154. }
  155. }
  156. return $rc;
  157. }
  158. # ------------------------------------------------------------------------------
  159. # SYNOPSIS
  160. # ($rc, $used) = $obj->run_get_used();
  161. #
  162. # DESCRIPTION
  163. # This method is part of run(). It attempts to work out or set up the $used
  164. # file. ($used is undef if it is not defined in a branch for this file.)
  165. # ------------------------------------------------------------------------------
  166. sub run_get_used {
  167. my ($self) = @_;
  168. my $rc = 1;
  169. my $used;
  170. my @sources = ($self->src->[0]);
  171. my $src_status = 'B';
  172. if (defined ($self->src->[0]->cache)) {
  173. # File exists in base branch
  174. for my $i (1 .. @{ $self->src } - 1) {
  175. if (defined ($self->src->[$i]->cache)) {
  176. # Detect changes in this file between base branch and branch $i
  177. push @sources, $self->src->[$i]
  178. if &compare ($self->src->[0]->cache, $self->src->[$i]->cache);
  179. } else {
  180. # File deleted in branch $i
  181. @sources = ($self->src->[$i]);
  182. last unless $self->conflict eq 'override';
  183. }
  184. }
  185. if ($rc) {
  186. if (@sources > 2) {
  187. if ($self->conflict eq 'fail') {
  188. # On conflict, fail in fail mode
  189. w_report 'ERROR: ', $self->pkgname,
  190. ': modified in 2+ branches in fail conflict mode.';
  191. $rc = undef;
  192. } elsif ($self->conflict eq 'override') {
  193. $used = $sources[-1]->cache;
  194. $src_status = 'O';
  195. } else {
  196. # On conflict, attempt to merge in merge mode
  197. ($rc, $used) = $self->run_get_used_by_merge (@sources);
  198. $src_status = 'G' if $rc;
  199. }
  200. } else {
  201. # 0 or 1 change, use last source
  202. if (defined $sources[-1]->cache) {
  203. $used = $sources[-1]->cache;
  204. $src_status = 'M' if @sources > 1;
  205. } else {
  206. $src_status = 'D';
  207. }
  208. }
  209. }
  210. } else {
  211. # File does not exist in base branch
  212. @sources = ($self->src->[-1]);
  213. $used = $self->src->[1]->cache;
  214. $src_status = (defined ($used) ? 'A' : 'D');
  215. if ($self->conflict ne 'override' and defined ($used)) {
  216. for my $i (1 - @{ $self->src } .. -2) {
  217. # Allow this only if files are the same in all branches
  218. my $file = $self->src->[$i]->cache;
  219. if ((not defined ($file)) or &compare ($used, $file)) {
  220. w_report 'ERROR: ', $self->pkgname, ': cannot merge:',
  221. ' not found in base branch,',
  222. ' but differs in subsequent branches.';
  223. $rc = undef;
  224. last;
  225. } else {
  226. unshift @sources, $self->src->[$i];
  227. }
  228. }
  229. }
  230. }
  231. $self->src_status ($src_status);
  232. $self->src_actual (\@sources);
  233. return ($rc, $used);
  234. }
  235. # ------------------------------------------------------------------------------
  236. # SYNOPSIS
  237. # ($rc, $used) = $obj->run_get_used_by_merge(@soruces);
  238. #
  239. # DESCRIPTION
  240. # This method is part of run_get_used(). It attempts to merge the files in
  241. # @sources and return a temporary file $used. @sources should be an array of
  242. # Fcm::ExtractSrc objects. On success, $rc will be set to true.
  243. # ------------------------------------------------------------------------------
  244. sub run_get_used_by_merge {
  245. my ($self, @sources) = @_;
  246. my $rc = 1;
  247. # Get temporary file
  248. my ($fh, $used) = &tempfile ('fcm.ext.merge.XXXXXX', UNLINK => 1);
  249. close $fh or die $used, ': cannot close';
  250. for my $i (2 .. @sources - 1) {
  251. # Invoke the diff3 command to merge
  252. my $mine = ($i == 2 ? $sources[1]->cache : $used);
  253. my $older = $sources[0]->cache;
  254. my $yours = $sources[$i]->cache;
  255. my @command = (
  256. $self->setting (qw/TOOL DIFF3/),
  257. split (/\s+/, $self->setting (qw/TOOL DIFF3FLAGS/)),
  258. $mine, $older, $yours,
  259. );
  260. my $code;
  261. my @out = &run_command (
  262. \@command,
  263. METHOD => 'qx',
  264. ERROR => 'ignore',
  265. PRINT => $self->verbose > 1,
  266. RC => \$code,
  267. TIME => $self->verbose > 2,
  268. );
  269. if ($code) {
  270. # Failure, report and return
  271. my $m = ($code == 1)
  272. ? 'cannot resolve conflicts:'
  273. : $self->setting (qw/TOOL DIFF3/) . 'command failed';
  274. w_report 'ERROR: ', $self->pkgname, ': merge - ', $m;
  275. if ($code == 1 and $self->verbose) {
  276. for (0 .. $i) {
  277. my $src = $sources[$_]->uri eq $sources[$_]->cache
  278. ? $sources[$_]->cache
  279. : ($sources[$_]->uri . '@' . $sources[$_]->rev);
  280. w_report ' source[', $_, ']=', $src;
  281. }
  282. for (0 .. $i) {
  283. w_report ' cache', $_, '=', $sources[$_]->cache;
  284. }
  285. w_report @out if $self->verbose > 2;
  286. }
  287. $rc = undef;
  288. last;
  289. } else {
  290. # Success, write result to temporary file
  291. open FILE, '>', $used or die $used, ': cannot open (', $!, ')';
  292. print FILE @out;
  293. close FILE or die $used, ': cannot close (', $!, ')';
  294. # File permission, use most permissive combination of $mine and $yours
  295. my $perm = ((stat($mine))[2] & 07777) | ((stat($yours))[2] & 07777);
  296. chmod ($perm, $used);
  297. }
  298. }
  299. return ($rc, $used);
  300. }
  301. # ------------------------------------------------------------------------------
  302. # SYNOPSIS
  303. # $rc = $obj->run_update($used_file);
  304. #
  305. # DESCRIPTION
  306. # This method is part of run(). It compares the $used_file with the one in
  307. # the destination. If the file does not exist in the destination or if its
  308. # content is out of date, the destination is updated with the content in the
  309. # $used_file. Returns true on success.
  310. # ------------------------------------------------------------------------------
  311. sub run_update {
  312. my ($self, $used_file) = @_;
  313. my ($is_diff, $is_diff_in_perms, $is_in_prev, $rc) = (1, 1, undef, 1);
  314. # Compare with the previous version if it exists
  315. DEST:
  316. for my $i (0 .. @{$self->dest()} - 1) {
  317. my $prev_file = File::Spec->catfile($self->dest()->[$i], $self->pkgname());
  318. if (-f $prev_file) {
  319. $is_in_prev = $i;
  320. $is_diff = compare($used_file, $prev_file);
  321. $is_diff_in_perms = (stat($used_file))[2] != (stat($prev_file))[2];
  322. last DEST;
  323. }
  324. }
  325. if (!$is_diff && !$is_diff_in_perms) {
  326. return $rc;
  327. }
  328. # Update destination
  329. my $dest_file = File::Spec->catfile($self->dest()->[0], $self->pkgname());
  330. if ($is_diff) {
  331. my $dir = dirname($dest_file);
  332. if (!-d $dir) {
  333. mkpath($dir);
  334. }
  335. $rc = copy($used_file, $dest_file);
  336. }
  337. $rc &&= chmod((stat($used_file))[2] & oct(7777), $dest_file);
  338. if ($rc) {
  339. $self->dest_status(
  340. $is_in_prev ? 'a'
  341. : defined($is_in_prev) ? 'M'
  342. : 'A'
  343. );
  344. }
  345. return $rc;
  346. }
  347. # ------------------------------------------------------------------------------
  348. 1;
  349. __END__