New URL for NEMO forge!   http://forge.nemo-ocean.eu

Since March 2022 along with NEMO 4.2 release, the code development moved to a self-hosted GitLab.
This present forge is now archived and remained online for history.
SVN.pm in vendors/lib/FCM/Util/Locator – NEMO

source: vendors/lib/FCM/Util/Locator/SVN.pm @ 10669

Last change on this file since 10669 was 10669, checked in by nicolasmartin, 5 years ago

Import latest FCM release from Github into the repository for testing

File size: 13.1 KB
Line 
1# ------------------------------------------------------------------------------
2# (C) British Crown Copyright 2006-17 Met Office.
3#
4# This file is part of FCM, tools for managing and building source code.
5#
6# FCM is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# FCM is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with FCM. If not, see <http://www.gnu.org/licenses/>.
18# ------------------------------------------------------------------------------
19use strict;
20use warnings;
21
22# ------------------------------------------------------------------------------
23package FCM::Util::Locator::SVN;
24use base qw{FCM::Class::CODE};
25
26use File::Temp;
27use POSIX qw{setlocale LC_ALL};
28use Time::Piece;
29
30our %ACTION_OF = (
31    as_invariant      => \&_as_invariant,
32    can_work_with     => \&_can_work_with,
33    can_work_with_rev => \&_can_work_with_rev,
34    cat               => \&_cat,
35    dir               => \&_dir,
36    export            => \&_export,
37    export_ok         => \&_export_ok,
38    find              => \&_find,
39    origin            => \&_origin,
40    parse             => \&_parse,
41    reader            => \&_reader,
42    read_property     => \&_read_property,
43    test_exists       => \&_test_exists,
44    trunk_at_head     => \&_trunk_at_head,
45);
46
47my %PATTERN_OF = (
48    REVISION       => qr{\A(?:\d+|HEAD|BASE|COMMITTED|PREV|\{[^\}]+\})\z}ixms,
49    TARGET_PEG     => qr{\A(.+?)(?:@([^/@]+))?\z}xms,
50    URL_COMPONENTS => qr{\A([A-Za-z][\w\.\+\-]*://)([^/]*)(/?.*)\z}xms,
51);
52
53my %INFO_OF = (
54    'Path'                => 'path',
55    'URL'                 => 'URL',
56    'Repository Root'     => 'repos_root_URL',
57    'Repository UUID'     => 'repos_UUID',
58    'Revision'            => 'rev',
59    'Node Kind'           => 'kind',
60    'Last Changed Author' => 'last_changed_author',
61    'Last Changed Rev'    => 'last_changed_rev',
62    'Last Changed Date'   => 'last_changed_date',
63    # FIXME: currently omitting lock info and other WC info.
64);
65
66my %INFO_KIND_OF = (directory => 'dir', file => 'file');
67
68my %INFO_MOD_OF = (
69    last_changed_date => \&_svn_info_last_changed_date,
70    kind => sub {exists($INFO_KIND_OF{$_[0]}) ? $INFO_KIND_OF{$_[0]} : $_[0]},
71);
72
73# Creates the class.
74__PACKAGE__->class(
75    {type_util_of => '%', util => '&'},
76    {action_of => \%ACTION_OF},
77);
78
79# Returns the invariant version of $value.
80sub _as_invariant {
81    my ($attrib_ref, $value) = @_;
82    my ($target, $revision) = _parse($attrib_ref, $value);
83    if (!$attrib_ref->{util}->uri_match($target) && !$revision) {
84        return;
85    }
86    $revision ||= $attrib_ref->{util}->uri_match($target) ? 'HEAD' : 'BASE';
87    my %info_of;
88    _svn_info(
89        $attrib_ref,
90        sub {%info_of = %{$_[0]}},
91        [$value],
92    );
93    return _parse_simple($attrib_ref, $info_of{URL}, $info_of{rev});
94}
95
96# Returns true if $value looks like a legitimate SVN target.
97sub _can_work_with {
98    my ($attrib_ref, $value) = @_;
99    my ($scheme) = $attrib_ref->{util}->uri_match($value);
100    if ($scheme && grep {$_ eq $scheme} qw{svn file svn+ssh http https}) {
101        return $value;
102    }
103    my ($target, $revision) = _parse($attrib_ref, $value);
104    my $url;
105    local($@);
106    eval {_svn_info($attrib_ref, sub {$url = $_[0]->{URL}}, [$target])};
107    return $url;
108}
109
110# Returns true if $revision looks like a legitimate SVN revision specifier.
111sub _can_work_with_rev {
112    my ($attrib_ref, $revision) = @_;
113    if (!defined($revision)) {
114        return;
115    }
116    return $revision =~ $PATTERN_OF{REVISION};
117}
118
119# Joins @paths to the end of $value.
120sub _cat {
121    my ($attrib_ref, $value, @paths) = @_;
122    my ($target, $rev) = _parse($attrib_ref, $value);
123    my $is_uri = $attrib_ref->{util}->uri_match($target);
124    $target
125        = $is_uri ? join('/', $target, @paths)
126        :           $attrib_ref->{type_util_of}{fs}->cat($target, @paths)
127        ;
128    _parse_simple($attrib_ref, _tidy($target), $rev);
129}
130
131# Returns the directory containing $value.
132sub _dir {
133    my ($attrib_ref, $value) = @_;
134    my ($target, $revision) = _parse($attrib_ref, $value);
135    if ($attrib_ref->{util}->uri_match($target)) {
136        my ($leader, $auth, $trailer) = $target =~ $PATTERN_OF{URL_COMPONENTS};
137        if (!$trailer) {
138            return _parse($attrib_ref, $target, $revision);
139        }
140        $trailer =~ s{/+ [^/]* \z}{}xms;
141        $target = $leader . ($auth ? $auth : q{}) . $trailer;
142    }
143    else {
144        $target = $attrib_ref->{type_util_of}{fs}->dir($target);
145    }
146    _parse_simple($attrib_ref, $target, $revision);
147}
148
149# Export $value to $dest.
150sub _export {
151    my ($attrib_ref, $value, $dest) = @_;
152    _run_svn_simple($attrib_ref, 'export', [$value, $dest], {quiet => undef});
153}
154
155# Returns true if $value is a URL.
156sub _export_ok {
157    my ($attrib_ref, $value) = @_;
158    $attrib_ref->{util}->uri_match($value);
159}
160
161# Searches directory tree of $value.
162sub _find {
163    my ($attrib_ref, $value, $callback) = @_;
164    if (!$attrib_ref->{util}->uri_match($value)) {
165        return $attrib_ref->{type_util_of}{fs}->find($value, $callback);
166    }
167    _svn_info(
168        $attrib_ref,
169        sub {
170            my ($info_ref) = @_;
171            $callback->(
172                $info_ref->{URL} . '@' . $info_ref->{rev},
173                {   is_dir        => $info_ref->{kind} eq 'dir',
174                    last_mod_rev  => $info_ref->{last_changed_rev},
175                    last_mod_time => $info_ref->{last_changed_date},
176                    ns            => $info_ref->{path},
177                },
178            );
179        },
180        [$value],
181        {recursive => undef},
182    );
183    return 1;
184}
185
186# Returns the URL version of $value.
187sub _origin {
188    my ($attrib_ref, $value) = @_;
189    my ($target, $revision) = _parse($attrib_ref, $value);
190    if ($attrib_ref->{util}->uri_match($target)) {
191        return _parse_simple($attrib_ref, $value);
192    }
193    $revision ||= 'BASE';
194    _as_invariant(
195        $attrib_ref,
196        scalar(_parse_simple($attrib_ref, $target, $revision)),
197    );
198}
199
200# In list context, returns ($target, $revision). In scalar context, returns
201# "$target@$revision".
202sub _parse {
203    my ($attrib_ref, $value, $revision) = @_;
204    my ($target, $peg_revision) = $value =~ $PATTERN_OF{TARGET_PEG};
205    if ($peg_revision) {
206        $revision = $peg_revision;
207    }
208    $target
209        = $attrib_ref->{util}->uri_match($value)
210        ? _tidy($target)
211        : $attrib_ref->{type_util_of}{fs}->parse($target)
212        ;
213    _parse_simple($attrib_ref, $target, $revision);
214}
215
216# Same as _parse, but without _tidy.
217sub _parse_simple {
218    my ($attrib_ref, $value, $revision) = @_;
219    (
220        wantarray() ? ($value, $revision)
221        :             $value . ($revision ? q{@} . $revision : q{})
222    );
223}
224
225# Returns a named property of a Subversion target.
226sub _read_property {
227    my ($attrib_ref, $value, $name) = @_;
228    _run_svn_simple($attrib_ref, 'pg', [$name, $value]);
229}
230
231# Returns a reader (file handle) for a given Subversion target.
232sub _reader {
233    my ($attrib_ref, $value) = @_;
234    my ($target, $revision) = _parse($attrib_ref, $value);
235    if ($attrib_ref->{util}->uri_match($target) || $revision) {
236        return _run_svn_handle($attrib_ref, 'cat', [$value]);
237    }
238    else {
239        return $attrib_ref->{type_util_of}{fs}->reader($target);
240    }
241}
242
243# Helper for _run_svn_*, generates the command.
244sub _run_svn_command {
245    my ($attrib_ref, $key, $args_ref, $option_ref) = @_;
246    $args_ref   ||= [];
247    $option_ref ||= {};
248    my @options;
249    while (my ($key, $value) = each(%{$option_ref})) {
250        push(@options, '--' . $key . (defined($value) ? '=' . $value : q{}));
251    }
252    ['svn', $key, @options, @{$args_ref}];
253}
254
255# Runs "svn", sending standard output to a file handle.
256sub _run_svn_handle {
257    my ($attrib_ref, $key, $args_ref, $option_ref) = @_;
258    local($ENV{LANG}) = $ENV{LANG};
259    if (setlocale(LC_ALL, 'en_GB')) {
260        $ENV{LANG} = 'en_GB';
261    }
262    my $handle = File::Temp->new();
263    my $rc = $attrib_ref->{util}->shell(
264        _run_svn_command(@_),
265        {e => \my($err), o => sub {print($handle $_[0])}},
266    );
267    if ($rc || (!tell($handle) && $err)) { # cat, info, etc may return 0 on err
268        chomp($err);
269        die("$err\n");
270    }
271    seek($handle, 0, 0);
272    return $handle;
273}
274
275# Runs a simple "svn" command.
276sub _run_svn_simple {
277    my ($attrib_ref, $key, $args_ref, $option_ref) = @_;
278    local($ENV{LANG}) = $ENV{LANG};
279    if (setlocale(LC_ALL, 'en_GB')) {
280        $ENV{LANG} = 'en_GB';
281    }
282    my $value_hash_ref
283        = $attrib_ref->{util}->shell_simple(_run_svn_command(@_));
284    if ($value_hash_ref->{rc}) {
285        die($value_hash_ref);
286    }
287    $value_hash_ref->{o};
288}
289
290# Runs "svn info".
291sub _svn_info {
292    my ($attrib_ref, $callback_ref, $args_ref, $option_ref) = @_;
293    my $handle = _run_svn_handle($attrib_ref, 'info', $args_ref, $option_ref);
294    my %hash;
295    while (my $line = readline($handle)) {
296        chomp($line);
297        if ($line) {
298            my ($key, $value) = split(qr{:\s*}msx, $line, 2);
299            if (exists($INFO_OF{$key})) {
300                my $id = $INFO_OF{$key};
301                $hash{$id}
302                    = exists($INFO_MOD_OF{$id}) ? $INFO_MOD_OF{$id}->($value)
303                    :                             $value
304                    ;
305            }
306        }
307        else {
308            $callback_ref->(\%hash);
309        }
310    }
311}
312
313# Parse last changed date from "svn info".
314sub _svn_info_last_changed_date {
315    my $text = (split(qr{\s+\(}msx, $_[0], 2))[0];
316    my $head = Time::Piece->strptime(substr($text, 0, -6), '%Y-%m-%d %H:%M:%S');
317    my $tail = substr($text, -5);
318    my ($tz_sign, $tz_h, $tz_m) = $tail =~ qr{([\-\+])(\d\d)(\d\d)}msx;
319    $head->epoch() - int($tz_sign . 1) * ($tz_h * 3600 + $tz_m * 60);
320}
321
322# Return a true value if the location $value exists.
323sub _test_exists {
324    my ($attrib_ref, $value) = @_;
325    my $url;
326    eval {_svn_info($attrib_ref, sub {$url = $_[0]->{URL}}, [$value])};
327    return $url;
328}
329
330# Returns a tidied version of a Subversion URL.
331sub _tidy {
332    my ($url) = @_;
333    my ($leader, $auth, $trailer) = $url =~ $PATTERN_OF{URL_COMPONENTS};
334    if (!$trailer) {
335        return $url;
336    }
337    my @tidied_names;
338    my %handler_of = (
339        q{}   => sub {push(@tidied_names, $_[0])},
340        q{.}  => sub {},
341        q{..} => sub {if (@tidied_names > 1) {pop(@tidied_names)}},
342    );
343    for my $name (split(qr{/+}xms, $trailer)) {
344        my $handler
345            = exists($handler_of{$name}) ? $handler_of{$name} : $handler_of{q{}};
346        $handler->($name);
347    }
348    return $leader . ($auth ? $auth : q{}) . join(q{/}, @tidied_names);
349}
350
351# Returns trunk@HEAD for a URL.
352sub _trunk_at_head {
353    my ($attrib_ref, $target) = @_;
354    if (!$attrib_ref->{util}->uri_match($target)) {
355        return;
356    }
357    _cat($attrib_ref, $target, 'trunk@HEAD');
358}
359
360# ------------------------------------------------------------------------------
3611;
362__END__
363
364=head1 NAME
365
366FCM::Util::Locator::SVN
367
368=head1 SYNOPSIS
369
370    use FCM::Util;
371    $util = FCM::Util->new(\%attrib);
372    $reader = $util->loc_reader($locator);
373
374
375=head1 DESCRIPTION
376
377This is part of L<FCM::Util|FCM::Util>. Provides utilities for Subversion
378targets.
379
380=head1 ATTRIBUTES
381
382=over 4
383
384=item util
385
386The L<FCM::Util|FCM::Util> object that initialised this object.
387
388=head1 METHODS
389
390=over 4
391
392=item $util->as_invariant($value)
393
394Returns the invariant version of $value. For example, if the current HEAD
395revision is 1234, and $value is C<svn://foo/bar/baz> or
396C<svn://foo/bar/baz@HEAD>, it will return C<svn://foo/bar/baz@1234>.
397
398=item $util->can_work_with($value)
399
400Returns the URL form of $value (true) if $value is a valid SVN target.
401
402=item $util->can_work_with_rev($revision)
403
404Returns true if $revision looks like a legitimate SVN revision specifier.
405
406=item $util->cat($value,@paths)
407
408Join @paths to the end of $value.
409
410=item $util->dir($value)
411
412Returns the directory name of $value.
413
414=item $util->export($value,$dest)
415
416Exports a clean directory tree of $value to $dest.
417
418=item $util->export_ok($value)
419
420Returns true if $value is a URL. (It is not safe to export a working copy.)
421
422=item $util->find($value,$callback)
423
424Searches directory tree of $value.
425
426=item $util->origin($value)
427
428Returns the URL version of $value.
429
430=item $util->parse($value,$revision)
431
432In scalar context, returns a string in C<TARGET@REV> for $value. In list
433context, given C<TARGET@REV> returns (C<TARGET>, C<REV>). If $value has a peg
434revision, it overrides the specified $revision.
435
436=item $util->reader($value)
437
438Returns a file handle for reading the content in $value, if possible.
439
440=item $util->read_property($value,$name)
441
442Returns the value of a property $name of $value.
443
444=item $util->test_exists($value)
445
446Return a true value if the location $value exists.
447
448=item $util->trunk_at_head($value)
449
450Returns "$value/trunk@HEAD' if $value is a URI or undef otherwise.
451
452=back
453
454=head1 COPYRIGHT
455
456(C) Crown copyright Met Office. All rights reserved.
457
458=cut
Note: See TracBrowser for help on using the repository browser.