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