1package SVN::Simple::Edit; 2@ISA = qw(SVN::Delta::Editor); 3$VERSION = '0.28'; 4use strict; 5use SVN::Core; 6use SVN::Delta; 7 8=head1 NAME 9 10SVN::Simple::Edit - A simple interface for driving svn delta editors 11 12=head1 SYNOPSIS 13 14 my $edit = SVN::Simple::Edit->new 15 (_editor => [SVN::Repos::get_commit_editor($repos, "file://$repospath", 16 '/', 'root', 'FOO', \&committed)], 17 ); 18 19 $edit->open_root($fs->youngest_rev); 20 $edit->add_directory ('trunk'); 21 $edit->add_file ('trunk/filea'); 22 $edit->modify_file ("trunk/fileb", "content", $checksum); 23 $edit->delete_entry ("trunk/filec"); 24 $edit->close_edit (); 25 ... 26 $edit->copy_directory ('branches/a, trunk, 0); 27 28=head1 DESCRIPTION 29 30SVN::Simple::Edit wraps the subversion delta editor with a perl 31friendly interface and then you could easily drive it for describing 32changes to a tree. A common usage is to wrap the commit editor, so 33you could make commits to a subversion repository easily. 34 35This also means you can not supply the C<$edit> object as an 36delta_editor to other API, and that's why this module is named 37B<::Edit> instead of B<::Editor>. 38 39See L<SVN::Simple::Editor> for simple interface implementing a delta editor. 40 41=head1 PARAMETERS 42 43=head2 for constructor 44 45=over 46 47=item _editor 48 49The editor that will receive delta editor calls. 50 51=item missing_handler 52 53Called when parent directory are not opened yet, could be: 54 55=over 56 57=item \&SVN::Simple::Edit::build_missing 58 59Always build parents if you don't open them explicitly. 60 61=item \&SVN::Simple::Edit::open_missing 62 63Always open the parents if you don't create them explicitly. 64 65=item SVN::Simple::Edit::check_missing ([$root]) 66 67Check if the path exists on $root. Open it if so, otherwise create it. 68 69=back 70 71=item root 72 73The default root to use by SVN::Simple::Edit::check_missing. 74 75=item base_path 76 77The base path the edit object is created to send delta editor calls. 78 79=item noclose 80 81Do not close files or directories. This might make non-sorted 82operations on directories/files work. 83 84=back 85 86=head1 METHODS 87 88Note: Don't expect all editors will work with operations not sorted in 89DFS order. 90 91=over 92 93=item open_root ($base_rev) 94 95=item add_directory ($path) 96 97=item open_directory ($path) 98 99=item copy_directory ($path, $from, $fromrev) 100 101=item add_file ($path) 102 103=item open_file ($path) 104 105=item copy_file ($path, $from, $fromrev) 106 107=item delete_entry ($path) 108 109=item change_dir_prop ($path, $propname, $propvalue) 110 111=item change_file_prop ($path, $propname, $propvalue) 112 113=item close_edit () 114 115=back 116 117=cut 118 119require File::Spec::Unix; 120 121sub splitpath { File::Spec::Unix->splitpath(@_) }; 122sub canonpath { File::Spec::Unix->canonpath(@_) }; 123 124sub build_missing { 125 my ($self, $path) = @_; 126 $self->add_directory ($path); 127} 128 129sub open_missing { 130 my ($self, $path) = @_; 131 $self->open_directory ($path); 132} 133 134sub check_missing { 135 my ($root) = @_; 136 return sub { 137 my ($self, $path) = @_; 138 $root ||= $self->{root}; 139 $root->check_path (($self->{base_path} || '')."/$path") == $SVN::Node::none ? 140 $self->add_directory ($path) : $self->open_directory($path); 141 } 142} 143 144sub new { 145 my $class = shift; 146 my $self = $class->SUPER::new(@_); 147 $self->{BATON} = {}; 148 $self->{missing_handler} ||= \&build_missing; 149 return $self; 150} 151 152sub set_target_revision { 153 my ($self, $target_revision) = @_; 154 $self->SUPER::set_target_revision ($target_revision); 155} 156 157sub _rev_from_root { 158 my ($self, $path) = @_; 159 $path = "/$path" if $path; 160 $path ||= ''; 161 return $self->{root}->node_created_rev($self->{base_path}.$path); 162} 163 164sub open_root { 165 my ($self, $base_revision) = @_; 166 $base_revision ||= $self->_rev_from_root () if $self->{root}; 167 $self->{BASE} = $base_revision; 168 $self->{BATON}{''} = $self->SUPER::open_root 169 ($base_revision, ${$self->{pool}}); 170} 171 172sub find_pbaton { 173 my ($self, $path, $missing_handler) = @_; 174 use Carp; 175 return $self->{BATON}{''} unless $path; 176 my (undef, $dir, undef) = splitpath($path); 177 $dir = canonpath ($dir); 178 179 180 return $self->{BATON}{$dir} if exists $self->{BATON}{$dir}; 181 182 $missing_handler ||= $self->{missing_handler}; 183 die "unable to get baton for directory $dir" 184 unless $missing_handler; 185 186 my $pbaton = &$missing_handler ($self, $dir); 187 188 return $pbaton; 189} 190 191sub close_other_baton { 192 my ($self, $path) = @_; 193 return if $self->{noclose}; 194 my (undef, $dir, undef) = splitpath($path); 195 $dir = canonpath ($dir); 196 197 for (reverse sort grep { !$dir || substr ($_, 0, length ($dir)+1) eq "$dir/"} 198 keys %{$self->{BATON}}) { 199 next unless $path; 200 my $baton = $self->{BATON}{$path}; 201 if ($self->{FILES}{$path}) { 202 $self->SUPER::close_file ($baton, undef, $self->{pool}); 203 } 204 else { 205 $self->SUPER::close_directory ($baton, $self->{pool}); 206 } 207 delete $self->{FILES}{$path}; 208 delete $self->{BATON}{$path}; 209 } 210} 211 212sub open_directory { 213 my ($self, $path, $pbaton) = @_; 214 $path =~ s|^/||; 215 $self->close_other_baton ($path); 216 $pbaton ||= $self->find_pbaton ($path); 217 my $base_revision = $self->_rev_from_root ($path) if $self->{root}; 218 $base_revision ||= $self->{BASE}; 219 $self->{BATON}{$path} = $self->SUPER::open_directory ($path, $pbaton, 220 $base_revision, 221 $self->{pool}); 222} 223 224sub add_directory { 225 my ($self, $path, $pbaton) = @_; 226 $path =~ s|^/||; 227 $self->close_other_baton ($path); 228 $pbaton ||= $self->find_pbaton ($path); 229 $self->{BATON}{$path} = $self->SUPER::add_directory ($path, $pbaton, undef, 230 -1, $self->{pool}); 231} 232 233sub copy_directory { 234 my ($self, $path, $from, $fromrev, $pbaton) = @_; 235 $path =~ s|^/||; 236 $pbaton ||= $self->find_pbaton ($path); 237 $self->{BATON}{$path} = $self->SUPER::add_directory ($path, $pbaton, $from, 238 $fromrev, 239 $self->{pool}); 240} 241 242sub open_file { 243 my ($self, $path, $pbaton) = @_; 244 $path =~ s|^/||; 245 $self->close_other_baton ($path); 246 $pbaton ||= $self->find_pbaton ($path); 247 my $base_revision = $self->_rev_from_root ($path) if $self->{root}; 248 $base_revision ||= $self->{BASE}; 249 $self->{FILES}{$path} = 1; 250 $self->{BATON}{$path} = $self->SUPER::open_file ($path, $pbaton, 251 $base_revision, 252 $self->{pool}); 253} 254 255sub add_file { 256 my ($self, $path, $pbaton) = @_; 257 $path =~ s|^/||; 258 $self->close_other_baton ($path); 259 $pbaton ||= $self->find_pbaton ($path); 260 $self->{FILES}{$path} = 1; 261 $self->{BATON}{$path} = $self->SUPER::add_file ($path, $pbaton, undef, -1, 262 $self->{pool}); 263} 264 265sub copy_file { 266 my ($self, $path, $from, $fromrev, $pbaton) = @_; 267 $path =~ s|^/||; 268 $pbaton ||= $self->find_pbaton ($path); 269 $self->{BATON}{$path} = $self->SUPER::add_file ($path, $pbaton, $from, 270 $fromrev, $self->{pool}); 271} 272 273sub modify_file { 274 my ($self, $path, $content, $targetchecksum) = @_; 275 $path =~ s|^/|| unless ref($path); 276 my $baton = ref($path) ? $path : 277 ($self->{BATON}{$path} || $self->open_file ($path)); 278 my $ret = $self->apply_textdelta ($baton, undef, $self->{pool}); 279 280 return unless $ret && $ret->[0]; 281 282 if (ref($content) && $content->isa ('GLOB')) { 283 my $md5 = SVN::TxDelta::send_stream ($content, 284 @$ret, 285 $self->{pool}); 286 die "checksum mistach ($md5) vs ($targetchecksum)" if $targetchecksum 287 && $targetchecksum ne $md5; 288 } 289 else { 290 SVN::_Delta::svn_txdelta_send_string ($content, @$ret, $self->{pool}); 291 } 292} 293 294sub delete_entry { 295 my ($self, $path, $pbaton) = @_; 296 my $base_revision; 297 $path =~ s|^/||; 298 $pbaton ||= $self->find_pbaton ($path, \&open_missing); 299 300 $base_revision = $self->_rev_from_root ($path) if $self->{root}; 301 $base_revision ||= $self->{BASE}; 302 $self->SUPER::delete_entry ($path, $base_revision, $pbaton, $self->{pool}); 303} 304 305sub change_file_prop { 306 my ($self, $path, $key, $value) = @_; 307 $path =~ s|^/|| unless ref($path); 308 my $baton = ref($path) ? $path : 309 ($self->{BATON}{$path} || $self->open_file ($path)); 310 $self->SUPER::change_file_prop ($baton, $key, $value, $self->{pool}); 311} 312 313sub change_dir_prop { 314 my ($self, $path, $key, $value) = @_; 315 $path =~ s|^/|| unless ref($path); 316 my $baton = ref($path) ? $path : 317 ($self->{BATON}{$path} || $self->open_directory ($path)); 318 $self->SUPER::change_dir_prop ($baton, $key, $value, $self->{pool}); 319} 320 321sub close_file { 322 my ($self, $path, $checksum) = @_; 323 my $baton = $self->{BATON}{$path} or die "not opened"; 324 delete $self->{BATON}{$path}; 325 $self->SUPER::close_file ($baton, $checksum, $self->{pool}); 326} 327 328sub close_directory { 329 my ($self, $path) = @_; 330 my $baton = $self->{BATON}{$path} or die "not opened"; 331 delete $self->{BATON}{$path}; 332 $self->SUPER::close_directory ($baton, $self->{pool}); 333} 334 335sub close_edit { 336 my ($self) = @_; 337 $self->close_other_baton (''); 338 $self->SUPER::close_edit ($self->{pool}); 339} 340 341sub abort_edit { 342 my ($self) = @_; 343 344 $self->SUPER::abort_edit ($self->{pool}); 345} 346 347=head1 AUTHORS 348 349Chia-liang Kao E<lt>clkao@clkao.orgE<gt> 350 351=head1 COPYRIGHT 352 353Copyright 2003-2004 by Chia-liang Kao E<lt>clkao@clkao.orgE<gt>. 354 355This program is free software; you can redistribute it and/or modify it 356under the same terms as Perl itself. 357 358See L<http://www.perl.com/perl/misc/Artistic.html> 359 360=cut 3611; 362