1 | # ------------------------------------------------------------------------------ |
---|
2 | # Copyright (C) 2006-2021 British Crown (Met Office) & Contributors. |
---|
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 | # ------------------------------------------------------------------------------ |
---|
19 | use strict; |
---|
20 | use warnings; |
---|
21 | |
---|
22 | # ------------------------------------------------------------------------------ |
---|
23 | package FCM::System::CM; |
---|
24 | use base qw{FCM::Class::CODE}; |
---|
25 | |
---|
26 | use Cwd qw{cwd}; |
---|
27 | use FCM1::Cm; |
---|
28 | use FCM1::Interactive; |
---|
29 | use FCM::Context::Event; |
---|
30 | use FCM::Context::Locator; |
---|
31 | use FCM::System::CM::CommitMessage; |
---|
32 | use FCM::System::CM::Prompt; |
---|
33 | use FCM::System::CM::ResolveConflicts qw{_cm_resolve_conflicts}; |
---|
34 | use FCM::System::CM::SVN; |
---|
35 | use FCM::System::Exception; |
---|
36 | use FCM::Util::Exception; |
---|
37 | use File::Spec::Functions qw{catfile}; |
---|
38 | use List::Util qw{first}; |
---|
39 | use Storable qw{dclone}; |
---|
40 | |
---|
41 | # The (keys) named actions of this class and (values) their implementations. |
---|
42 | our %ACTION_OF = ( |
---|
43 | cm_branch_create => \&_cm_branch_create, |
---|
44 | cm_branch_delete => _fcm1_func(\&FCM1::Cm::cm_branch_delete), |
---|
45 | cm_branch_diff => _fcm1_func(\&FCM1::Cm::cm_branch_diff), |
---|
46 | cm_branch_info => _fcm1_func(\&FCM1::Cm::cm_branch_info), |
---|
47 | cm_branch_list => \&_cm_branch_list, |
---|
48 | cm_commit => _fcm1_func(\&FCM1::Cm::cm_commit), |
---|
49 | cm_checkout => \&_cm_checkout, |
---|
50 | cm_check_missing => _fcm1_func( |
---|
51 | \&FCM1::Cm::cm_check_missing, |
---|
52 | _opt_mod_st_check_handler_func('WC_STATUS_PATH'), |
---|
53 | ), |
---|
54 | cm_check_unknown => _fcm1_func( |
---|
55 | \&FCM1::Cm::cm_check_unknown, |
---|
56 | _opt_mod_st_check_handler_func('WC_STATUS_PATH'), |
---|
57 | ), |
---|
58 | cm_diff => \&_cm_diff, |
---|
59 | cm_loc_layout => \&_cm_loc_layout, |
---|
60 | cm_merge => _fcm1_func(\&FCM1::Cm::cm_merge), |
---|
61 | cm_mkpatch => _fcm1_func(\&FCM1::Cm::cm_mkpatch), |
---|
62 | cm_project_create => \&_cm_project_create, |
---|
63 | cm_resolve_conflicts => \&_cm_resolve_conflicts, |
---|
64 | cm_switch => _fcm1_func( |
---|
65 | \&FCM1::Cm::cm_switch, _opt_mod_st_check_handler_func('WC_STATUS'), |
---|
66 | ), |
---|
67 | cm_update => _fcm1_func( |
---|
68 | \&FCM1::Cm::cm_update, _opt_mod_st_check_handler_func('WC_STATUS'), |
---|
69 | ), |
---|
70 | svn => \&_svn, |
---|
71 | ); |
---|
72 | |
---|
73 | # Alias |
---|
74 | my $E = 'FCM::System::Exception'; |
---|
75 | |
---|
76 | # Creates the class. |
---|
77 | __PACKAGE__->class( |
---|
78 | { commit_message_util => '&', |
---|
79 | gui => '$', |
---|
80 | prompt => '&', |
---|
81 | svn => '&', |
---|
82 | util => '&', |
---|
83 | }, |
---|
84 | {init => \&_init, action_of => \%ACTION_OF}, |
---|
85 | ); |
---|
86 | |
---|
87 | sub _init { |
---|
88 | my ($attrib_ref) = @_; |
---|
89 | if (!defined(FCM1::Keyword::get_util())) { |
---|
90 | FCM1::Keyword::set_util($attrib_ref->{util}); |
---|
91 | } |
---|
92 | if ($attrib_ref->{'gui'}) { |
---|
93 | FCM1::Interactive::set_impl( |
---|
94 | 'FCM1::Interactive::InputGetter::GUI', |
---|
95 | {geometry => $attrib_ref->{gui}}, |
---|
96 | ); |
---|
97 | } |
---|
98 | $attrib_ref->{prompt} = FCM::System::CM::Prompt->new({ |
---|
99 | gui => $attrib_ref->{gui}, util => $attrib_ref->{util}, |
---|
100 | }); |
---|
101 | $attrib_ref->{commit_message_util} = FCM::System::CM::CommitMessage->new({ |
---|
102 | gui => $attrib_ref->{gui}, |
---|
103 | util => $attrib_ref->{util}, |
---|
104 | }); |
---|
105 | $attrib_ref->{svn} = FCM::System::CM::SVN->new({util => $attrib_ref->{util}}); |
---|
106 | FCM1::Cm::set_util($attrib_ref->{util}); |
---|
107 | FCM1::Cm::set_commit_message_util($attrib_ref->{commit_message_util}); |
---|
108 | FCM1::Cm::set_svn_util($attrib_ref->{svn}); |
---|
109 | } |
---|
110 | |
---|
111 | # Create a branch in a project. |
---|
112 | sub _cm_branch_create { |
---|
113 | my ($attrib_ref, $option_ref, @args) = @_; |
---|
114 | _parse_args($attrib_ref, $option_ref, \@args); |
---|
115 | my ($name, $source) = @args; |
---|
116 | # Check branch name |
---|
117 | if (!$name || $name !~ qr{\A[\w\.\-/]+\z}msx) { |
---|
118 | return $E->throw($E->CM_BRANCH_NAME, $name ? $name : q{}); |
---|
119 | } |
---|
120 | # Determine ticket list with name |
---|
121 | if (!$option_ref->{ticket} && $name =~ qr{\A[1-9]\d*([_\-][1-9]\d*)*\z}msx) { |
---|
122 | $option_ref->{ticket} = [split(qr{[_\-]}msx, $name)]; |
---|
123 | } |
---|
124 | # Check source |
---|
125 | $source ||= cwd() . '@HEAD'; |
---|
126 | my $layout = $attrib_ref->{svn}->get_layout($source); |
---|
127 | my $root = $layout->get_root(); |
---|
128 | my $source_rev = $layout->get_peg_rev(); |
---|
129 | my $project = $layout->get_project(); |
---|
130 | my $source_branch = $layout->get_branch(); |
---|
131 | if (!defined($project)) { |
---|
132 | return $E->throw($E->CM_BRANCH_SOURCE, $source); |
---|
133 | } |
---|
134 | my @project_paths = split(qr{/}msx, $project); |
---|
135 | |
---|
136 | # Determine whether to create a branch of a branch |
---|
137 | if (!$option_ref->{'branch-of-branch'} || !$source_branch) { |
---|
138 | $source_branch = 'trunk'; |
---|
139 | } |
---|
140 | $source = join('/', $root, @project_paths, $source_branch) |
---|
141 | . '@' . $source_rev; |
---|
142 | my $source_commit_rev |
---|
143 | = $attrib_ref->{svn}->get_info($source)->[0]->{'commit:revision'}; |
---|
144 | $source = join('/', $root, @project_paths, $source_branch) |
---|
145 | . '@' . $source_commit_rev; |
---|
146 | $attrib_ref->{util}->event( |
---|
147 | FCM::Context::Event->CM_BRANCH_CREATE_SOURCE, $source, $source_rev, |
---|
148 | ); |
---|
149 | |
---|
150 | # Handle multiple tickets |
---|
151 | $option_ref->{ticket} ||= []; |
---|
152 | $option_ref->{ticket} = [ |
---|
153 | sort |
---|
154 | {$a <=> $b} |
---|
155 | map |
---|
156 | {s{\A#}{}msx; $_} |
---|
157 | split(qr{,}msx, join(q{,}, @{$option_ref->{ticket}})) |
---|
158 | ]; |
---|
159 | |
---|
160 | # Determine the sub-directory names of the branch |
---|
161 | # FIXME: hard coded legacy! |
---|
162 | my %layout_config = %{$layout->get_config()}; |
---|
163 | my @names; |
---|
164 | if ($layout_config{'template-branch'}) { |
---|
165 | my $template = $layout_config{'template-branch'}; |
---|
166 | if ( index($template, '{category}') >= 0 |
---|
167 | || index($template, '{owner}') >= 0 |
---|
168 | ) { |
---|
169 | $option_ref->{type} ||= 'dev::user'; |
---|
170 | $option_ref->{type} = lc($option_ref->{type}); |
---|
171 | $option_ref->{type} |
---|
172 | = $option_ref->{type} eq 'user' ? 'dev::user' |
---|
173 | : $option_ref->{type} eq 'share' ? 'dev::share' |
---|
174 | : $option_ref->{type} eq 'config' ? 'pkg::config' |
---|
175 | : $option_ref->{type} eq 'rel' ? 'pkg::rel' |
---|
176 | : $option_ref->{type} eq 'dev' ? 'dev::user' |
---|
177 | : $option_ref->{type} eq 'test' ? 'test::user' |
---|
178 | : $option_ref->{type} eq 'pkg' ? 'pkg::user' |
---|
179 | : $option_ref->{type} |
---|
180 | ; |
---|
181 | if (!grep {$option_ref->{type} eq $_} qw{ |
---|
182 | dev::share dev::user test::share test::user |
---|
183 | pkg::config pkg::rel pkg::share pkg::user |
---|
184 | }) { |
---|
185 | return $E->throw($E->CM_OPT_ARG, ['type', $option_ref->{type}]); |
---|
186 | } |
---|
187 | my %set = map {$_ => 1} split('::', $option_ref->{type}); |
---|
188 | if (index($template, '{category}') >= 0) { |
---|
189 | my $index = index($template, '{category}'); |
---|
190 | my $category = first {exists($set{$_})} qw{dev test pkg}; |
---|
191 | substr($template, $index, length('{category}'), $category); |
---|
192 | } |
---|
193 | if (index($template, '{owner}') >= 0) { |
---|
194 | my $index = index($template, '{owner}'); |
---|
195 | my $owner = exists($set{user}) |
---|
196 | ? $attrib_ref->{svn}->get_username($root) |
---|
197 | : first {exists($set{lc($_)})} qw{Share Config Rel}; |
---|
198 | substr($template, $index, length('{owner}'), $owner); |
---|
199 | } |
---|
200 | } |
---|
201 | if (index($template, '{name_prefix}') >= 0) { |
---|
202 | my $index = index($template, '{name_prefix}'); |
---|
203 | # Check revision flag is valid |
---|
204 | $option_ref->{'rev-flag'} ||= 'normal'; |
---|
205 | $option_ref->{'rev-flag'} = lc($option_ref->{'rev-flag'}); |
---|
206 | if (!grep {$_ eq $option_ref->{'rev-flag'}} qw{normal number none}) { |
---|
207 | return $E->throw( |
---|
208 | $E->CM_OPT_ARG, ['rev-flag', $option_ref->{'rev-flag'}]); |
---|
209 | } |
---|
210 | my $name_prefix = q{}; |
---|
211 | if ($option_ref->{'rev-flag'} ne 'none') { |
---|
212 | $name_prefix = 'r' . $source_commit_rev; |
---|
213 | if ($option_ref->{'rev-flag'} eq 'normal') { |
---|
214 | # Attempt to replace revision number with a keyword |
---|
215 | my $locator = FCM::Context::Locator->new($source); |
---|
216 | my $as_keyword = $attrib_ref->{util}->loc_as_keyword($locator); |
---|
217 | my ($u, $r) = $attrib_ref->{svn}->split_by_peg($as_keyword); |
---|
218 | if ($source_commit_rev ne $r) { |
---|
219 | $name_prefix = $r; |
---|
220 | } |
---|
221 | } |
---|
222 | |
---|
223 | # Add an underscore |
---|
224 | $name_prefix .= '_'; |
---|
225 | } |
---|
226 | substr($template, $index, length('{name_prefix}'), $name_prefix); |
---|
227 | } |
---|
228 | if (index($template, '{name}') >= 0) { |
---|
229 | my $index = index($template, '{name}'); |
---|
230 | substr($template, $index, length('{name}'), $name); |
---|
231 | } |
---|
232 | push(@names, split(qr{/+}msx, $template)); |
---|
233 | } |
---|
234 | else { |
---|
235 | push(@names, split(qr{/+}msx, $name)); |
---|
236 | } |
---|
237 | if ($layout_config{'depth-branch'} != scalar(@names)) { |
---|
238 | return $E->throw($E->CM_BRANCH_NAME, join('/', @names)); |
---|
239 | } |
---|
240 | if ($layout_config{'dir-branch'}) { |
---|
241 | unshift(@names, $layout_config{'dir-branch'}); |
---|
242 | } |
---|
243 | # Check whether the branch already exists |
---|
244 | my $target = join('/', $root, @project_paths, @names); |
---|
245 | my $target_url = eval {$attrib_ref->{svn}->get_info($target)->[0]->{url}}; |
---|
246 | $@ = undef; |
---|
247 | if ($target_url) { |
---|
248 | return $E->throw($E->CM_ALREADY_EXIST, $target_url); |
---|
249 | } |
---|
250 | |
---|
251 | # Message for the commit log |
---|
252 | my @tickets = @{$option_ref->{ticket}}; |
---|
253 | my @message = sprintf('%sCreated %s from %s@%d.' . "\n", |
---|
254 | (@tickets ? join(q{,}, map {'#' . $_} @tickets) . q{: } : q{}), |
---|
255 | join('/', q{}, @project_paths, @names), |
---|
256 | join('/', q{}, @project_paths, $source_branch), $source_commit_rev, |
---|
257 | ); |
---|
258 | |
---|
259 | # Create a temporary file for the commit log message |
---|
260 | my $commit_message_ctx = $attrib_ref->{commit_message_util}->ctx(); |
---|
261 | $commit_message_ctx->set_auto_part(join(q{}, @message)); |
---|
262 | $commit_message_ctx->set_info_part(sprintf("%s %s\n", 'A', $target)); |
---|
263 | if (!$option_ref->{'non-interactive'}) { |
---|
264 | $attrib_ref->{commit_message_util}->edit($commit_message_ctx); |
---|
265 | } |
---|
266 | $attrib_ref->{commit_message_util}->notify($commit_message_ctx); |
---|
267 | my $temp_handle |
---|
268 | = $attrib_ref->{commit_message_util}->temp($commit_message_ctx); |
---|
269 | |
---|
270 | # Check with the user to see if he/she wants to go ahead |
---|
271 | if ( !$option_ref->{'non-interactive'} |
---|
272 | && !$attrib_ref->{prompt}->question('BRANCH_CREATE') |
---|
273 | ) { |
---|
274 | return; |
---|
275 | } |
---|
276 | |
---|
277 | # Create the branch |
---|
278 | $attrib_ref->{svn}->call( |
---|
279 | 'copy', |
---|
280 | '--file', $temp_handle->filename(), |
---|
281 | '--parents', |
---|
282 | ($option_ref->{'svn-non-interactive'} ? '--non-interactive' : ()), |
---|
283 | ( defined($option_ref->{'password'}) |
---|
284 | ? ('--password', $option_ref->{'password'}) : () |
---|
285 | ), |
---|
286 | $source, |
---|
287 | $target, |
---|
288 | ); |
---|
289 | $attrib_ref->{util}->event(FCM::Context::Event->CM_CREATE_TARGET, $target); |
---|
290 | |
---|
291 | # Switch working copy to point to newly created branch |
---|
292 | if ($option_ref->{'switch'}) { |
---|
293 | $ACTION_OF{'cm_switch'}->($attrib_ref, $option_ref, $target); |
---|
294 | } |
---|
295 | |
---|
296 | $target; |
---|
297 | } |
---|
298 | |
---|
299 | # Filter lists branches in projects. |
---|
300 | sub _cm_branch_list { |
---|
301 | my ($attrib_ref, $option_ref, @args) = @_; |
---|
302 | _parse_args($attrib_ref, $option_ref, \@args); |
---|
303 | my $used_default_arg; |
---|
304 | if (!@args) { |
---|
305 | @args = cwd() . '@HEAD'; |
---|
306 | $used_default_arg = 1; |
---|
307 | } |
---|
308 | my %common_patterns_at; |
---|
309 | if ($option_ref->{'only'} && @{$option_ref->{'only'}}) { |
---|
310 | for (@{$option_ref->{'only'}}) { |
---|
311 | my ($depth, $pattern) = split(qr{:}msx, $_, 2); |
---|
312 | $common_patterns_at{$depth} ||= []; |
---|
313 | push(@{$common_patterns_at{$depth}}, $pattern); |
---|
314 | } |
---|
315 | } |
---|
316 | my $UTIL = $attrib_ref->{'util'}; |
---|
317 | ARG: |
---|
318 | for my $arg (@args) { |
---|
319 | my %patterns_at = %{dclone(\%common_patterns_at)}; |
---|
320 | my %info = eval {%{$attrib_ref->{svn}->get_info($arg)->[0]}}; |
---|
321 | if ($@) { |
---|
322 | if ($used_default_arg) { |
---|
323 | # Can't complain about a bad arg if we put it there. |
---|
324 | return $E->throw($E->SHELL, $@->{ctx}, $@->{ctx}->{e}); |
---|
325 | } |
---|
326 | return $E->throw($E->CM_ARG, $arg); |
---|
327 | } |
---|
328 | my $url = $info{'url'} . '@' . $info{'revision'}; |
---|
329 | my $layout = $attrib_ref->{svn}->get_layout($url); |
---|
330 | my $root = $layout->get_root(); |
---|
331 | my $rev = $layout->get_peg_rev(); |
---|
332 | my $project = $layout->get_project(); |
---|
333 | if (!defined($project)) { |
---|
334 | next ARG; |
---|
335 | } |
---|
336 | my $url_project = $root . ($project ? '/' . $project : q{}); |
---|
337 | my %layout_config = %{$layout->get_config()}; |
---|
338 | if ($layout_config{'level-owner-branch'} && !$option_ref->{'show-all'}) { |
---|
339 | my $level = $layout_config{'level-owner-branch'}; |
---|
340 | if ($option_ref->{'user'} && @{$option_ref->{'user'}}) { |
---|
341 | $patterns_at{$level} = [ |
---|
342 | map {'^' . $_ . '$'} |
---|
343 | map {split(qr{[,:]}msx, $_)} |
---|
344 | @{$option_ref->{'user'}} |
---|
345 | ]; |
---|
346 | } |
---|
347 | elsif (!%patterns_at) { |
---|
348 | my $owner = $attrib_ref->{svn}->get_username($root); |
---|
349 | $patterns_at{$level} = ['^' . $owner . '$']; |
---|
350 | } |
---|
351 | } |
---|
352 | my $url0 = $url_project; |
---|
353 | if ($layout_config{'dir-branch'}) { |
---|
354 | $url0 .= '/' . $layout_config{'dir-branch'}; |
---|
355 | } |
---|
356 | else { |
---|
357 | for my $key (qw{trunk tag}) { |
---|
358 | if ($layout_config{"dir-$key"}) { |
---|
359 | $patterns_at{1} ||= []; |
---|
360 | push( |
---|
361 | @{$patterns_at{1}}, |
---|
362 | '^(?!' . $layout_config{"dir-$key"} . '$)', |
---|
363 | ); |
---|
364 | } |
---|
365 | } |
---|
366 | } |
---|
367 | my @branches = $attrib_ref->{svn}->get_list( |
---|
368 | $url0 . '@' . $rev, |
---|
369 | sub { |
---|
370 | my ($this_url, $this_name, $is_dir, $depth) = @_; |
---|
371 | if ( exists($patterns_at{$depth}) |
---|
372 | && !grep {$this_name =~ /$_/} @{$patterns_at{$depth}} |
---|
373 | ) { |
---|
374 | return (0, 0); |
---|
375 | } |
---|
376 | my $can_return = $depth >= $layout_config{'depth-branch'}; |
---|
377 | ($can_return, ($is_dir && !$can_return)); |
---|
378 | }, |
---|
379 | ); |
---|
380 | if ($option_ref->{'url'}) { |
---|
381 | $UTIL->event( |
---|
382 | FCM::Context::Event->CM_BRANCH_LIST, |
---|
383 | $url_project . '@' . $rev, @branches, |
---|
384 | ); |
---|
385 | } |
---|
386 | else { |
---|
387 | $UTIL->event( |
---|
388 | FCM::Context::Event->CM_BRANCH_LIST, |
---|
389 | map {$UTIL->loc_as_keyword(FCM::Context::Locator->new($_))} |
---|
390 | ($url_project . '@' . $rev, @branches), |
---|
391 | ); |
---|
392 | } |
---|
393 | } |
---|
394 | } |
---|
395 | |
---|
396 | # Wraps "svn checkout". |
---|
397 | sub _cm_checkout { |
---|
398 | my ($attrib_ref, $option_ref, @args) = @_; |
---|
399 | _parse_args($attrib_ref, $option_ref, \@args); |
---|
400 | my $target = @args && !$attrib_ref->{util}->uri_match($args[-1]) |
---|
401 | ? $args[-1] : cwd(); |
---|
402 | my $info_entry = eval {$attrib_ref->{svn}->get_info($target)->[0]}; |
---|
403 | if ($@) { |
---|
404 | $@ = undef; # OK, not a working copy |
---|
405 | } |
---|
406 | elsif (grep {index($_, 'wc-info:') == 0} keys(%{$info_entry})) { |
---|
407 | return $E->throw($E->CM_CHECKOUT, [$target, $info_entry->{url}]); |
---|
408 | } |
---|
409 | $attrib_ref->{svn}->call('checkout', @args); |
---|
410 | } |
---|
411 | |
---|
412 | # Wraps "svn diff". |
---|
413 | sub _cm_diff { |
---|
414 | my ($attrib_ref, $option_ref, @args) = @_; |
---|
415 | _parse_args($attrib_ref, $option_ref, \@args); |
---|
416 | local(%ENV) = %ENV; |
---|
417 | $ENV{FCM_GRAPHIC_DIFF} |
---|
418 | ||= $attrib_ref->{util}->external_cfg_get('graphic-diff'); |
---|
419 | $attrib_ref->{svn}->call('diff', @args); |
---|
420 | } |
---|
421 | |
---|
422 | # Parse and print layout information of each target in @args. |
---|
423 | sub _cm_loc_layout { |
---|
424 | my ($attrib_ref, $option_ref, @args) = @_; |
---|
425 | _parse_args($attrib_ref, $option_ref, \@args); |
---|
426 | if (!@args) { |
---|
427 | @args = qw{.}; |
---|
428 | } |
---|
429 | my $OUT = sub { |
---|
430 | $attrib_ref->{util}->event(FCM::Context::Event->OUT, @_); |
---|
431 | }; |
---|
432 | my $not_first; |
---|
433 | for my $arg (@args) { |
---|
434 | if ($not_first) { |
---|
435 | $OUT->("\n"); |
---|
436 | } |
---|
437 | $not_first = 1; |
---|
438 | $OUT->("target: $arg\n"); |
---|
439 | my $layout = $attrib_ref->{svn}->get_layout($arg); |
---|
440 | $OUT->($layout->as_string()); |
---|
441 | } |
---|
442 | } |
---|
443 | |
---|
444 | # Create a new project in a repository. |
---|
445 | sub _cm_project_create { |
---|
446 | my ($attrib_ref, $option_ref, @args) = @_; |
---|
447 | _parse_args($attrib_ref, $option_ref, \@args); |
---|
448 | my ($name, $root_arg) = @args; |
---|
449 | # Check project name |
---|
450 | if (!$name || $name !~ qr{\A[\w\.\-/]+\z}msx) { |
---|
451 | return $E->throw($E->CM_PROJECT_NAME, $name); |
---|
452 | } |
---|
453 | # Check root |
---|
454 | if (!$root_arg) { |
---|
455 | return $E->throw($E->CM_REPOSITORY, q{}); |
---|
456 | } |
---|
457 | my $layout = $attrib_ref->{svn}->get_layout($root_arg); |
---|
458 | my $root = $layout->get_root(); |
---|
459 | if (!$root) { |
---|
460 | return $E->throw($E->CM_REPOSITORY, $root_arg); |
---|
461 | } |
---|
462 | |
---|
463 | # Check whether the depth of the project name is valid |
---|
464 | my %layout_config = %{$layout->get_config()}; |
---|
465 | my @names = split(qr{/+}msx, $name); |
---|
466 | my $depth_expected = $layout_config{'depth-project'}; |
---|
467 | if (defined($depth_expected) && $depth_expected != scalar(@names)) { |
---|
468 | return $E->throw($E->CM_PROJECT_NAME, join('/', @names)); |
---|
469 | } |
---|
470 | # Check whether the project (trunk) already exists |
---|
471 | my $target = join('/', $root, @names, $layout_config{'dir-trunk'}); |
---|
472 | my $target_url = eval {$attrib_ref->{svn}->get_info($target)->[0]->{url}}; |
---|
473 | $@ = undef; |
---|
474 | if ($target_url) { |
---|
475 | return $E->throw($E->CM_ALREADY_EXIST, $target_url); |
---|
476 | } |
---|
477 | |
---|
478 | # Message for the commit log |
---|
479 | my @message = sprintf("%s: new project.\n", join('/', @names)); |
---|
480 | |
---|
481 | # Create a temporary file for the commit log message |
---|
482 | my $commit_message_ctx = $attrib_ref->{commit_message_util}->ctx(); |
---|
483 | $commit_message_ctx->set_auto_part(join(q{}, @message)); |
---|
484 | $commit_message_ctx->set_info_part(sprintf("%s %s\n", 'A', $target)); |
---|
485 | if (!$option_ref->{'non-interactive'}) { |
---|
486 | $attrib_ref->{commit_message_util}->edit($commit_message_ctx); |
---|
487 | } |
---|
488 | $attrib_ref->{commit_message_util}->notify($commit_message_ctx); |
---|
489 | my $temp_handle |
---|
490 | = $attrib_ref->{commit_message_util}->temp($commit_message_ctx); |
---|
491 | |
---|
492 | # Check with the user to see if he/she wants to go ahead |
---|
493 | if ( !$option_ref->{'non-interactive'} |
---|
494 | && !$attrib_ref->{prompt}->question('PROJECT_CREATE') |
---|
495 | ) { |
---|
496 | return; |
---|
497 | } |
---|
498 | |
---|
499 | # Create the branch |
---|
500 | $attrib_ref->{svn}->call( |
---|
501 | 'mkdir', |
---|
502 | '--file', $temp_handle->filename(), |
---|
503 | '--parents', |
---|
504 | ($option_ref->{'svn-non-interactive'} ? '--non-interactive' : ()), |
---|
505 | ( defined($option_ref->{'password'}) |
---|
506 | ? ('--password', $option_ref->{'password'}) : () |
---|
507 | ), |
---|
508 | $target, |
---|
509 | ); |
---|
510 | $attrib_ref->{util}->event(FCM::Context::Event->CM_CREATE_TARGET, $target); |
---|
511 | |
---|
512 | $target; |
---|
513 | } |
---|
514 | |
---|
515 | # Returns a simple wrapper to FCM 1 FCM1::Cm functions. |
---|
516 | sub _fcm1_func { |
---|
517 | my ($action_ref, $opt_mod_ref) = @_; |
---|
518 | $opt_mod_ref ||= sub {}; |
---|
519 | sub { |
---|
520 | my ($attrib_ref, $option_ref, @args) = @_; |
---|
521 | _parse_args($attrib_ref, $option_ref, \@args); |
---|
522 | local(@ARGV) = @args; |
---|
523 | $opt_mod_ref->($option_ref); |
---|
524 | eval {$action_ref->($option_ref, @args)}; |
---|
525 | if ($@) { |
---|
526 | if (!FCM1::Cm::Abort->caught($@)) { |
---|
527 | die($@); |
---|
528 | } |
---|
529 | if (!($@->get_code() eq $@->NULL || $@->get_code() eq $@->USER)) { |
---|
530 | die($@); |
---|
531 | } |
---|
532 | $attrib_ref->{util}->event( |
---|
533 | FCM::Context::Event->CM_ABORT, lc($@->get_code()), |
---|
534 | ); |
---|
535 | $@ = undef; |
---|
536 | } |
---|
537 | return; |
---|
538 | }; |
---|
539 | } |
---|
540 | |
---|
541 | # Generate an option modifier to st_check_handler. |
---|
542 | sub _opt_mod_st_check_handler_func { |
---|
543 | my $key = shift(); |
---|
544 | sub { |
---|
545 | my $option_ref = shift(); |
---|
546 | if (!$option_ref->{'non-interactive'}) { |
---|
547 | $option_ref->{st_check_handler} = $FCM1::Cm::CLI_HANDLER_OF{$key}; |
---|
548 | } |
---|
549 | }; |
---|
550 | } |
---|
551 | |
---|
552 | # Expands keywords in arguments. |
---|
553 | sub _parse_args { |
---|
554 | my ($attrib_ref, $option_ref, $args_ref) = @_; |
---|
555 | # Location keywords |
---|
556 | my $UTIL = $attrib_ref->{util}; |
---|
557 | my $url; |
---|
558 | for my $arg (@{$args_ref}) { |
---|
559 | eval { |
---|
560 | my $locator = FCM::Context::Locator->new($arg); |
---|
561 | if ($UTIL->loc_what_type($locator) eq 'svn') { |
---|
562 | my $new_arg = $UTIL->loc_as_normalised($locator); |
---|
563 | my $SVN = $attrib_ref->{svn}; |
---|
564 | my ($new_arg_url, $new_arg_rev) = $SVN->split_by_peg($new_arg); |
---|
565 | my ( $arg_url, $arg_rev) = $SVN->split_by_peg($arg); |
---|
566 | if (index($arg_url, $UTIL->loc_kw_prefix() . ':') == 0) { |
---|
567 | $arg_url = $new_arg_url; |
---|
568 | } |
---|
569 | if ($arg_rev && $new_arg_rev && $arg_rev ne $new_arg_rev) { |
---|
570 | $arg_rev = $new_arg_rev; |
---|
571 | } |
---|
572 | $arg = $arg_url . ($arg_rev ? '@' . $arg_rev : q{}); |
---|
573 | $url ||= $new_arg_url; |
---|
574 | } |
---|
575 | }; |
---|
576 | if (my $e = $@) { |
---|
577 | if ( !FCM::Util::Exception->caught($e) |
---|
578 | || index($e->get_code(), 'LOCATOR_') != 0 |
---|
579 | ) { |
---|
580 | die($e); |
---|
581 | } |
---|
582 | $@ = undef; |
---|
583 | } |
---|
584 | } |
---|
585 | # Revision keywords |
---|
586 | $url ||= cwd(); |
---|
587 | my $in_opt_rev; |
---|
588 | for my $arg (@{$args_ref}) { |
---|
589 | my ($opt, $opt_arg); |
---|
590 | if ($in_opt_rev) { |
---|
591 | $in_opt_rev = 0; |
---|
592 | ($opt, $opt_arg) = (q{}, $arg); |
---|
593 | } |
---|
594 | elsif (grep {$_ eq $arg} qw{-c --change -r --revision}) { |
---|
595 | $in_opt_rev = 1; |
---|
596 | } |
---|
597 | else { |
---|
598 | ($opt, $opt_arg) |
---|
599 | = $arg =~ qr{\A(-[cr]|--(?:change|revision)=)(.*)\z}msx; |
---|
600 | } |
---|
601 | if ($opt_arg) { |
---|
602 | $arg = $opt . _parse_args_rev($attrib_ref, $url, $opt_arg); |
---|
603 | } |
---|
604 | } |
---|
605 | for my $key (grep {exists($option_ref->{$_})} qw{change revision}) { |
---|
606 | $option_ref->{$key} |
---|
607 | = _parse_args_rev($attrib_ref, $url, $option_ref->{$key}); |
---|
608 | } |
---|
609 | } |
---|
610 | |
---|
611 | # Expands revision keywords in an argument. |
---|
612 | sub _parse_args_rev { |
---|
613 | my ($attrib_ref, $url, $arg) = @_; |
---|
614 | my $UTIL = $attrib_ref->{util}; |
---|
615 | join( |
---|
616 | ':', |
---|
617 | map { |
---|
618 | my $rev = $_; |
---|
619 | my $locator = FCM::Context::Locator->new($url . '@' . $rev); |
---|
620 | local($@); |
---|
621 | my $value = eval{$UTIL->loc_as_normalised($locator)}; |
---|
622 | if ($value) { |
---|
623 | (my $url, $rev) = $attrib_ref->{svn}->split_by_peg($value); |
---|
624 | } |
---|
625 | $rev; |
---|
626 | } split(qr{:}msx, $arg, 2) |
---|
627 | ); |
---|
628 | } |
---|
629 | |
---|
630 | # Invokes a system "svn" call. |
---|
631 | sub _svn { |
---|
632 | my ($attrib_ref, $app, $option_ref, @args) = @_; |
---|
633 | _parse_args($attrib_ref, $option_ref, \@args); |
---|
634 | $attrib_ref->{svn}->call($app, @args); |
---|
635 | } |
---|
636 | |
---|
637 | #------------------------------------------------------------------------------- |
---|
638 | 1; |
---|
639 | __END__ |
---|
640 | |
---|
641 | =head1 NAME |
---|
642 | |
---|
643 | FCM::System::CM |
---|
644 | |
---|
645 | =head1 SYNOPSIS |
---|
646 | |
---|
647 | use FCM::System::CM; |
---|
648 | my $system = FCM::System::CM->new(\%attrib); |
---|
649 | my ($out, $err) = $system->svn({}, @args); |
---|
650 | |
---|
651 | =head1 DESCRIPTION |
---|
652 | |
---|
653 | The FCM code management sub-system. This is currently a thin adaptor of |
---|
654 | L<FCM1::Cm|FCM1::Cm>. |
---|
655 | |
---|
656 | =head1 METHODS |
---|
657 | |
---|
658 | =over 4 |
---|
659 | |
---|
660 | =item $class->new(\%attrib) |
---|
661 | |
---|
662 | Returns a new instance. This class should normally be initialised by |
---|
663 | L<FCM::System|FCM::System>. |
---|
664 | |
---|
665 | =item $system->cm_branch_create(\%option,@args) |
---|
666 | |
---|
667 | Implement the C<fcm branch-create> command. On success, return the branch name |
---|
668 | created. |
---|
669 | |
---|
670 | =item $system->cm_branch_list(\%option,@args) |
---|
671 | |
---|
672 | Implement the C<fcm branch-list> command. |
---|
673 | |
---|
674 | =item $system->cm_checkout(\%option,@args) |
---|
675 | |
---|
676 | Thin wrapper of the C<svn checkout> command. Ensure checkout to clean location. |
---|
677 | |
---|
678 | =item $system->cm_diff(\%option,@args) |
---|
679 | |
---|
680 | Thin wrapper of the C<svn diff> command. Allow --graphical option. |
---|
681 | |
---|
682 | =item $system->cm_loc_layout(\%option,@args) |
---|
683 | |
---|
684 | Implement the C<fcm loc-layout> command. |
---|
685 | |
---|
686 | =item $system->cm_project_create(\%option,@args) |
---|
687 | |
---|
688 | Implement the C<fcm project-create> command. |
---|
689 | |
---|
690 | =item $system->cm_branch_delete(\%option,@args) |
---|
691 | =item $system->cm_branch_info(\%option,@args) |
---|
692 | =item $system->cm_commit(\%option,@args) |
---|
693 | =item $system->cm_check_missing(\%option,@args) |
---|
694 | =item $system->cm_check_unknown(\%option,@args) |
---|
695 | =item $system->cm_merge(\%option,@args) |
---|
696 | =item $system->cm_mkpatch(\%option,@args) |
---|
697 | =item $system->cm_resolve_conflicts(\%option,@args) |
---|
698 | =item $system->cm_switch(\%option,@args) |
---|
699 | =item $system->cm_update(\%option,@args) |
---|
700 | |
---|
701 | Thin adaptors for the corresponding code management functions in |
---|
702 | L<FCM1::Cm|FCM1::Cm>. |
---|
703 | |
---|
704 | =item $system->svn($app,\%option,@args) |
---|
705 | |
---|
706 | Invokes a system call to L<svn|svn> $app with @args. %option is not currently |
---|
707 | used, but is left in the argument list for compatibility with the other methods. |
---|
708 | |
---|
709 | =back |
---|
710 | |
---|
711 | =head1 COPYRIGHT |
---|
712 | |
---|
713 | Copyright (C) 2006-2021 British Crown (Met Office) & Contributors. |
---|
714 | |
---|
715 | =cut |
---|