1# BEGIN BPS TAGGED BLOCK {{{
2# COPYRIGHT:
3#
4# This software is Copyright (c) 2003-2008 Best Practical Solutions, LLC
5#                                          <clkao@bestpractical.com>
6#
7# (Except where explicitly superseded by other copyright notices)
8#
9#
10# LICENSE:
11#
12#
13# This program is free software; you can redistribute it and/or
14# modify it under the terms of either:
15#
16#   a) Version 2 of the GNU General Public License.  You should have
17#      received a copy of the GNU General Public License along with this
18#      program.  If not, write to the Free Software Foundation, Inc., 51
19#      Franklin Street, Fifth Floor, Boston, MA 02110-1301 or visit
20#      their web page on the internet at
21#      http://www.gnu.org/copyleft/gpl.html.
22#
23#   b) Version 1 of Perl's "Artistic License".  You should have received
24#      a copy of the Artistic License with this package, in the file
25#      named "ARTISTIC".  The license is also available at
26#      http://opensource.org/licenses/artistic-license.php.
27#
28# This work is distributed in the hope that it will be useful, but
29# WITHOUT ANY WARRANTY; without even the implied warranty of
30# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
31# General Public License for more details.
32#
33# CONTRIBUTION SUBMISSION POLICY:
34#
35# (The following paragraph is not intended to limit the rights granted
36# to you to modify and distribute this software under the terms of the
37# GNU General Public License and is only of importance to you if you
38# choose to contribute your changes and enhancements to the community
39# by submitting them to Best Practical Solutions, LLC.)
40#
41# By intentionally submitting any modifications, corrections or
42# derivatives to this work, or any other work intended for use with SVK,
43# to Best Practical Solutions, LLC, you confirm that you are the
44# copyright holder for those contributions and you grant Best Practical
45# Solutions, LLC a nonexclusive, worldwide, irrevocable, royalty-free,
46# perpetual, license to use, copy, create derivative works based on
47# those contributions, and sublicense and distribute those contributions
48# and any derivatives thereof.
49#
50# END BPS TAGGED BLOCK }}}
51package SVK::Command::Copy;
52use strict;
53use SVK::Version;  our $VERSION = $SVK::VERSION;
54use base qw( SVK::Command::Mkdir );
55use SVK::Util qw( get_anchor abs2rel splitdir is_uri make_path is_path_inside);
56use SVK::I18N;
57use SVK::Logger;
58
59sub options {
60    ($_[0]->SUPER::options,
61     'q|quiet'         => 'quiet',
62     'r|revision=s' => 'rev');
63}
64
65sub parse_arg {
66    my ($self, @arg) = @_;
67    return if @arg < 1;
68
69    push @arg, '' if @arg == 1;
70
71    my $dst = pop(@arg);
72    die loc ("Copy destination can't be URI.\n")
73	if is_uri ($dst);
74
75    die loc ("More than one URI found.\n")
76	if (grep {is_uri($_)} @arg) > 1;
77    my @src;
78
79    if ( my $target = eval { $self->{xd}->target_from_copath_maybe($dst) }) {
80        $dst = $target;
81	# don't allow new uri in source when target is copath
82	@src = (map {$self->arg_co_maybe
83			 ($_, $dst->isa('SVK::Path::Checkout')
84			  ? loc ("path '%1' is already a checkout", $dst->report)
85			  : undef)} @arg);
86    }
87    else {
88	@src = (map {$self->arg_co_maybe ($_)} @arg);
89        # Asking the user for copy destination.
90        # In this case, first magically promote ourselves to "cp -p".
91        # (otherwise it hurts when user types //deep/directory/name)
92        $self->{parent} = 1;
93
94        # -- make a sane default here for mirroring --
95        my $default = undef;
96        if (@src == 1 and $src[0]->path =~ m{/mirror/([^/]+)$}) {
97            $default = "/" . $src[0]->depotname . "/$1";
98        }
99
100        my $path = $self->prompt_depotpath("copy", $default);
101
102        if ($dst eq '.') {
103            $self->{_checkout_path} = (splitdir($path))[-1];
104        }
105        else {
106            $self->{_checkout_path} = $dst;
107        }
108
109        $dst = $self->arg_depotpath("$path/");
110    }
111
112    return (@src, $dst);
113}
114
115sub lock {
116    my $self = shift;
117    $self->lock_coroot($_[-1]);
118}
119
120sub handle_co_item {
121    my ($self, $src, $dst) = @_;
122    $src = $src->as_depotpath;
123    die loc ("Path %1 does not exist.\n", $src->path_anchor)
124	if $src->root->check_path ($src->path_anchor) == $SVN::Node::none;
125    my ($copath, $report) = ($dst->copath, $dst->report);
126    die loc ("Path %1 already exists.\n", $copath)
127	if -e $copath;
128    my ($entry, $schedule) = $self->{xd}->get_entry($copath, 1);
129    $src->normalize; $src->anchorify;
130    $self->ensure_parent($dst);
131    $dst->anchorify;
132
133    my $notify = $self->{quiet} ? SVK::Notify->new(quiet => 1) : undef;
134    # if SVK::Merge could take src being copath to do checkout_delta
135    # then we have 'svk cp copath... copath' for free.
136    # XXX: use editor::file when svkup branch is merged
137    my ($editor, $inspector, %cb) = $dst->get_editor
138	( ignore_checksum => 1, quiet => 1,
139	  check_only => $self->{check_only},
140	  update => 1, ignore_keywords => 1,
141	);
142    SVK::Merge->new (%$self, repos => $dst->repos, nodelay => 1,
143		     report => $report, notify => $notify,
144		     base => $src->new (path => '/', revision => 0),
145		     src => $src, dst => $dst)
146	    ->run
147		($editor, %cb, inspector => $inspector);
148
149    $self->{xd}{checkout}->store
150	($copath, { revision => undef });
151    # XXX: can the schedule be something other than delete ?
152    $self->{xd}{checkout}->store ($copath, {'.schedule' => $schedule ? 'replace' : 'add',
153					    scheduleanchor => $copath,
154					    '.copyfrom' => $src->path,
155					    '.copyfrom_rev' => $src->revision});
156}
157
158sub handle_direct_item {
159    my ($self, $editor, $anchor, $m, $src, $dst, $other_call) = @_;
160    $src->normalize;
161    # if we have targets, ->{path} must exist
162    if (!$self->{parent} && $dst->{targets} && !$dst->root->check_path ($dst->path_anchor)) {
163	die loc ("Parent directory %1 doesn't exist, use -p.\n", $dst->report);
164    }
165    my ($path, $rev) = ($src->path, $src->revision);
166    my $baton = $editor->add_directory (abs2rel ($dst->path, $anchor => undef, '/'), 0, $path, $rev);
167    $other_call->($baton) if $other_call;
168    $editor->close_directory($baton);
169    $editor->adjust_last_anchor;
170}
171
172sub _unmodified {
173    my ($self, $target) = @_;
174    my (@modified, @unknown);
175    $target = $self->{xd}->target_condensed($target); # anchor
176    $self->{xd}->checkout_delta
177	( $target->for_checkout_delta,
178	  xdroot => $target->create_xd_root,
179	  editor => SVK::Editor::Status->new
180	  ( notify => SVK::Notify->new
181	    ( cb_flush => sub { push @modified, $_[0] })),
182	  cb_unknown => sub { push @unknown, $_[1] } );
183
184    if (@modified || @unknown) {
185	my @reports = sort map { loc ("%1 is modified.\n", $target->report_copath ($_)) } @modified;
186	push @reports, sort map { loc ("%1 is unknown.\n", $target->report_copath ($_)) } @unknown;
187	die join("", @reports);
188    }
189}
190
191sub check_src {
192    my ($self, @src) = @_;
193    for my $src (@src) {
194	$src->revision($self->resolve_revision($src, $self->{rev})) if defined $self->{rev};
195	$self->apply_revision($src);
196	next unless $src->isa('SVK::Path::Checkout');
197	$self->_unmodified ($src->new);
198    }
199}
200
201sub run {
202    my ($self, @src) = @_;
203    my $dst = pop @src;
204
205    return loc("Different depots.\n") unless $dst->same_repos(@src);
206    my $m = $self->under_mirror($dst);
207    if ( $m && !$dst->same_source(@src) ) {
208        $logger->error(loc("You are trying to copy across different mirrors."));
209        die loc( "Create an empty directory %1, and run smerge --baseless %2 %3.\n",
210            $dst->report, $src[0]->report, $dst->report )
211          if $#src == 0 && $dst->isa('SVK::Path');
212        return 1;
213    }
214    $self->check_src (@src);
215    # XXX: check dst to see if the copy is obstructured or missing parent
216    my $fs = $dst->repos->fs;
217    if ($dst->isa('SVK::Path::Checkout')) {
218	return loc("%1 is not a directory.\n", $dst->report)
219	    if $#src > 0 && !-d $dst->copath;
220	return loc("%1 is not a versioned directory.\n", $dst->report)
221	    if -d $dst->copath && $dst->root->check_path($dst->path) != $SVN::Node::dir;
222
223	my @cpdst;
224	for (@src) {
225	    my $cpdst = $dst->new;
226	    # implicit target for "cp x y z dir/"
227	    if (-d $cpdst->copath) {
228		if ($_->path_anchor eq $cpdst->path_anchor) {
229		    $logger->warn(loc("Ignoring %1 as source.", $_->report));
230		    next;
231		}
232		if ( is_path_inside($cpdst->path_anchor, $_->path_anchor) ) {
233		    die loc("Invalid argument: copying directory %1 into itself.\n", $_->report);
234		}
235		$cpdst->descend ($_->path_anchor =~ m|/([^/]+)/?$|)
236	    }
237	    die loc ("Path %1 already exists.\n", $cpdst->report)
238		if -e $cpdst->copath;
239	    push @cpdst, [$_, $cpdst];
240	}
241	$self->handle_co_item(@$_) for @cpdst;
242    }
243    else {
244	if ($dst->root->check_path($dst->path_anchor) != $SVN::Node::dir) {
245	    die loc ("Copying more than one source requires %1 to be directory.\n", $dst->report)
246		if $#src > 0;
247	    $dst->anchorify;
248	}
249	$self->get_commit_message ();
250	my ($anchor, $editor) = $self->get_dynamic_editor ($dst);
251	for (@src) {
252            eval {
253                $self->handle_direct_item ($editor, $anchor, $m, $_,
254                                           $dst->{targets} ? $dst :
255                                           $dst->new (targets => [$_->path_anchor =~ m|/([^/]+)/?$|]));
256            };
257            if ($@) {
258                my $err = $@; # make sure not to lose it
259                # Clean up transaction.
260                $editor->abort_edit;
261                die $err;
262            }
263	}
264	$self->finalize_dynamic_editor ($editor);
265    }
266
267    if (defined( my $copath = $self->{_checkout_path} )) {
268        my $checkout = $self->command ('checkout');
269	$checkout->getopt ([]);
270        my @arg = $checkout->parse_arg ('/'.$dst->depotname.$dst->path, $copath);
271        $checkout->lock (@arg);
272        $checkout->run (@arg);
273    }
274
275    return;
276}
277
2781;
279
280__DATA__
281
282=head1 NAME
283
284SVK::Command::Copy - Make a versioned copy
285
286=head1 SYNOPSIS
287
288 copy DEPOTPATH1 DEPOTPATH2
289 copy DEPOTPATH [PATH]
290
291=head1 OPTIONS
292
293 -r [--revision] REV    : act on revision REV instead of the head revision
294 -p [--parent]          : create intermediate directories as required
295 -q [--quiet]           : print as little as possible
296 -m [--message] MESSAGE : specify commit message MESSAGE
297 -F [--file] FILENAME   : read commit message from FILENAME
298 --template             : use the specified message as the template to edit
299 --encoding ENC         : treat -m/-F value as being in charset encoding ENC
300 -P [--patch] NAME      : instead of commit, save this change as a patch
301 -S [--sign]            : sign this change
302 -C [--check-only]      : try operation but make no changes
303 --direct               : commit directly even if the path is mirrored
304
305