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