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::Editor::XD;
52use strict;
53use SVK::Version;  our $VERSION = $SVK::VERSION;
54
55use SVN::Delta;
56use base qw(SVK::Editor::Checkout);
57use SVK::I18N;
58use autouse 'File::Path' => qw(rmtree);
59use autouse 'SVK::Util'  => qw( get_anchor get_depot_anchor md5_fh );
60use Class::Autouse qw( SVK::Editor::Composite );
61
62=head1 NAME
63
64SVK::Editor::XD - An editor for modifying svk checkout copies
65
66=head1 SYNOPSIS
67
68$editor = SVK::Editor::XD->new
69    ( path => $path,
70      target => $target,
71      oldroot => $fs->revision_root ($fromrev),
72      newroot => $fs->revision_root ($torev),
73      xd => $xd,
74      get_copath => sub { ... },
75      get_path => sub { ... },
76    );
77
78
79=head1 DESCRIPTION
80
81SVK::Editor::XD modifies existing checkout copies at the paths
82translated by the get_copath callback, according to the incoming
83editor calls.  The path in the depot is translated with the get_path
84callback.
85
86There are two modes, one is for applying changes to checkout copy as
87external modification, like merging changes. The other is update mode,
88which is used for bringing changes from depot to checkout copies.
89
90=head1 PARAMETERS
91
92In addition to the paramters to L<SVK::Editor::Checkout>:
93
94=over
95
96=item target
97
98The target path of the editor calls.  Used for deciding if the root's
99meta data needs to be updated in update mode.
100
101=item xd
102
103L<SVK::XD> object.
104
105=item oldroot
106
107Old root before the editor calls.
108
109=item newroot
110
111New root after the editor calls.
112
113=item update
114
115Working in update mode.
116
117=item get_path
118
119A callback to translate paths in editor calls to path in depot.
120
121=item ignore_keywords
122
123Don't do keyword translations (svn:keywords property).
124
125=back
126
127=cut
128
129sub open_root {
130    my ($self, $base_revision) = @_;
131    $self->{signature} ||= SVK::XD::Signature->new (root => $self->{xd}->cache_directory)
132	if $self->{update};
133    return $self->SUPER::open_root($base_revision);
134}
135
136sub get_base {
137    my ($self, $path, $copath, $checksum) = @_;
138    my $dpath = $path;
139    $self->{get_path}->($dpath);
140
141    my ($dir,$file) = get_anchor (1, $copath);
142    my $basename = "$dir.svk.$file.base";
143
144    rename ($copath, $basename)
145	or warn loc("rename %1 to %2 failed: %3", $copath, $basename, $!), return;
146
147    my $base = SVK::XD::get_fh ($self->{oldroot}, '<', $dpath, $basename);
148    if (!$self->{ignore_checksum} && $checksum) {
149	my $md5 = md5_fh ($base);
150	die loc("source checksum mismatch for %1 (%2 vs %3)", $path, $md5, $checksum) if $md5 ne $checksum;
151	seek $base, 0, 0;
152    }
153
154    return [$base, $basename, -l $basename ? () : [stat($base)]];
155}
156
157sub get_fh {
158    my ($self, $path, $copath) = @_;
159    my ($dpath, $spath) = ($path, $path);
160    $self->{get_path}->($dpath);
161    $self->{get_store_path}->($spath);
162    # XXX: should test merge to co with keywords
163    delete $self->{props}{$path}{'svn:keywords'}
164	if !$self->{update} or $self->{ignore_keywords};
165    my $fh = SVK::XD::get_fh ($self->{newroot}, '>', $spath, $copath,
166			      $self->{added}{$path} ? $self->{props}{$path} || {}: undef)
167	or warn "can't open $path: $!", return;
168    return $fh;
169}
170
171sub close_file {
172    my $self = shift;
173    my $path = shift;
174    my $added = $self->{added}{$path};
175    $self->SUPER::close_file($path, @_);
176    return unless defined $path;
177    my $copath = $path;
178    $self->{get_copath}($copath);
179    if ($self->{update}) {
180	my (undef, $file) = get_anchor (1, $copath);
181	# populate signature cache for added files only, because
182	# modified file might be edited from merge editor, and thus
183	# actually unclean.  There should be notification from merge
184	# editor in the future, or to update the cache in cb_localmod
185	# for modified entries.
186	$self->{cursignature}[-1]->changed ($file)
187	    if $added;
188	$self->{xd}{checkout}->store ($copath, {revision => $self->{revision}}, {override_descendents => 0});
189	$self->{xd}->fix_permission ($copath, $self->{exe}{$path})
190	    if exists $self->{exe}{$path};
191    }
192
193    delete $self->{props}{$path};
194}
195
196sub add_file {
197    my $self = shift;
198    my ($path, $pdir, @arg) = @_;
199    my $ret = $self->SUPER::add_file(@_);
200    return undef unless defined $ret;
201    my $copath = $path;
202    $self->{get_copath}->($copath);
203    if (!$self->{update} && !$self->{check_only}) {
204	my ($anchor, $target, $editor);
205	if (defined $arg[0]) {
206	    ($anchor, $target) = get_depot_anchor(1, $path);
207	    $editor = SVK::Editor::Composite->new
208		( anchor => $anchor, anchor_baton => $pdir,
209		  target => $target, target_baton => $ret );
210	}
211	$self->_schedule_entry($copath, $editor, @arg);
212	if (defined $arg[0]) {
213	    # special treatment to turn this node into something with base
214	    delete $self->{added}{$path};
215	    delete $self->{iod}{$path};
216	}
217    }
218    return $ret;
219}
220
221sub add_directory {
222    my $self = shift;
223    my ($path, $pdir, @arg) = @_;
224    my $ret = $self->SUPER::add_directory (@_);
225    return undef unless defined $ret;
226    my $copath = $path;
227    $self->{get_copath}->($copath);
228    if (!$self->{update} && !$self->{check_only}) {
229	my $editor = SVK::Editor::Composite->new
230	    ( anchor => $path, anchor_baton => $pdir );
231	$self->_schedule_entry($copath, $editor, @arg);
232    }
233
234    push @{$self->{cursignature}}, $self->{signature}->load ($copath)
235	if $self->{update};
236    return $ret;
237}
238
239sub open_directory {
240    my ($self, $path, $pdir) = @_;
241    my $ret = $self->SUPER::open_directory ($path, $pdir);
242    return undef unless defined $ret;
243    # XXX: test if directory exists
244    if ($self->{update}) {
245	my $copath = $path;
246	$self->{get_copath}->($copath);
247	push @{$self->{cursignature}}, $self->{signature}->load ($copath);
248	$self->{cursignature}[-1]{keepold} = 1;
249    }
250    return $ret;
251}
252
253sub do_delete {
254    my $self = shift;
255    my ($path, $copath) = @_;
256    if ($self->{update}) {
257	$self->{xd}{checkout}->store
258	    ($copath,
259	     {revision => $self->{revision},
260	      '.deleted' => 1},
261            {override_descendents => 0});
262	return $self->SUPER::do_delete (@_)
263    }
264
265    $self->{get_path}($path);
266    $self->{xd}->do_delete( $self->{xd}->create_path_object
267			    ( copath_anchor => $copath,
268			      path => $path,
269			      repos => $self->{repos} ),
270			    quiet => 1 );
271}
272
273sub close_directory {
274    my ($self, $path) = @_;
275    return unless defined $path;
276    # the root is just an anchor
277    return if $self->{target} && !length($path);
278    my $copath = $path;
279    $self->{get_copath}($copath);
280    if ($self->{update}) {
281	# XXX: handle unwritable entries and back them up after the store
282	$self->{xd}{checkout}->store ($copath,
283                                      {revision => $self->{revision},
284                                       '.deleted' => undef},
285                                      {override_sticky_descendents => 1});
286	if (@{$self->{cursignature}}) {
287	    $self->{cursignature}[-1]->flush;
288	    pop @{$self->{cursignature}};
289	}
290    }
291}
292
293sub change_file_prop {
294    my ($self, $path, $name, $value) = @_;
295    return unless defined $path;
296    $self->{props}{$path}{$name} = $value
297	if $self->{added}{$path};
298    return if $self->{check_only};
299
300    my $copath = $path;
301    $self->{get_copath}($copath);
302
303    $self->{exe}{$path} = $value
304        if $name eq 'svn:executable' && $self->{update};
305
306    $self->{get_path}($path);
307    $self->{xd}->do_propset(
308        $self->{xd}->create_path_object(
309            copath_anchor => $copath,
310            path          => $path,
311            repos         => $self->{repos}
312        ),
313        quiet     => 1,
314        propname  => $name,
315        propvalue => $value,
316        adjust_only => $self->{update},
317    );
318}
319
320sub change_dir_prop {
321    my ($self, @arg) = @_;
322    $self->change_file_prop (@arg);
323}
324
325sub close_edit {
326    my ($self) = @_;
327}
328
329sub abort_edit {
330    my ($self) = @_;
331}
332
333# XXX: this should be splitted to prepare base and schedule entry
334sub _schedule_entry {
335    my ($self, $copath, $editor, $copyfrom, $copyfrom_rev) = @_;
336    my %copy;
337    if (defined $copyfrom) {
338	# Note that the delta run through $self here is for preparing
339	# the base for subsequent editor calls that operates on top of
340	# these.
341	my $fs = $self->{oldroot}->fs;
342	my $from_root = $fs->revision_root($copyfrom_rev);
343	$editor->{master_editor} = SVK::Editor::Checkout->new(%$self);
344	if (defined $editor->{target}) {
345	    # XXX: depot_delta can't generate single file fulltext.
346	    my $handle = $editor->apply_textdelta($editor->{target_baton}, '');
347		if ($handle && $#{$handle} >= 0) {
348		    SVN::TxDelta::send_stream($from_root->file_contents($copyfrom),
349					      @$handle);
350		    $editor->close_file($editor->{target_baton}, $from_root->file_md5_checksum($copyfrom));
351		}
352	}
353	else {
354	    $self->{xd}->depot_delta
355		( oldroot => $fs->revision_root(0),
356		  newroot => $from_root,
357		  oldpath => ['/', ''],
358		  newpath => $copyfrom,
359		  editor => $editor );
360	}
361	%copy = ( scheduleanchor => $copath,
362		  '.copyfrom' => $copyfrom,
363		  '.copyfrom_rev' => $copyfrom_rev );
364    }
365
366    my (undef, $schedule) = $self->{xd}->get_entry($copath, 1);
367    $self->{xd}{checkout}->store
368	($copath, { %copy, '.schedule' => $schedule eq 'delete' ? 'replace' : 'add' }, {override_descendents => 0});
369}
370
371
3721;
373