# BEGIN BPS TAGGED BLOCK {{{ # COPYRIGHT: # # This software is Copyright (c) 2003-2006 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) # # # LICENSE: # # # This program is free software; you can redistribute it and/or # modify it under the terms of either: # # a) Version 2 of the GNU General Public License. You should have # received a copy of the GNU General Public License along with this # program. If not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 or visit # their web page on the internet at # http://www.gnu.org/copyleft/gpl.html. # # b) Version 1 of Perl's "Artistic License". You should have received # a copy of the Artistic License with this package, in the file # named "ARTISTIC". The license is also available at # http://opensource.org/licenses/artistic-license.php. # # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # CONTRIBUTION SUBMISSION POLICY: # # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of the # GNU General Public License and is only of importance to you if you # choose to contribute your changes and enhancements to the community # by submitting them to Best Practical Solutions, LLC.) # # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with SVK, # to Best Practical Solutions, LLC, you confirm that you are the # copyright holder for those contributions and you grant Best Practical # Solutions, LLC a nonexclusive, worldwide, irrevocable, royalty-free, # perpetual, license to use, copy, create derivative works based on # those contributions, and sublicense and distribute those contributions # and any derivatives thereof. # # END BPS TAGGED BLOCK }}} package SVK::Editor::InteractiveStatus; use strict; use warnings; use Algorithm::Diff; use SVK::I18N; use SVN::Delta; use SVK::Version; our $VERSION = $SVK::VERSION; use SVK::Editor::Patch; use Algorithm::Diff; #our @ISA = qw(SVN::Delta::Editor); sub new { my ($class, @args) = @_; #my $self = $class->SUPER::new (@arg); my $self = bless {@args}, ref $class || $class; $self->{files} = {}; $self->{conflicts} = []; return $self; } sub close_edit { my ($self, $pool) = @_; if (@{$self->{conflicts}}) { my $msg = " ".join("\n ", @{$self->{conflicts}}); if (@{$self->{conflicts}} == 1) { $msg = loc("Conflict detected in:\n%1\n". "file. Do you want to skip it and commit other changes? (y/n) ", $msg); } else { $msg = loc("Conflict detected in:\n%1\n". "files. Do you want to skip those and commit other changes? (y/n) ", $msg); } if (SVK::Util::get_prompt($msg, qr/^[yn]$/i) =~ /[Nn]/) { $_->on_end_selection_phase($self) for @{$self->{actions}}; for (@{$self->{conflicts}}) { $self->{notify}->node_status($_, 'C'); $self->{notify}->flush($_); } $_->on_end_selection_phase_last_chance($self) for @{$self->{actions}}; return; } else { my (%actions, @actions); @actions = map {delete $self->{info}{$_}} @{$self->{conflicts}}; # flatten actions list. push @actions, @{$_->{children}} for @actions; @actions = map {$_->{id}} @actions; @actions{@actions} = 1; $self->{actions} = [grep {not exists $actions{$_->{id}}} @{$self->{actions}}]; } } my $ui = SVK::Editor::InteractiveStatus::UI->new($self); $ui->run; # Should this be on close edit??? $_->on_end_selection_phase($self) for @{$self->{actions}}; $_->on_end_selection_phase_last_chance($self) for @{$self->{actions}}; } sub abort_edit { my ($self, $pool) = @_; } sub open_root { my ($self, $baserev, $pool) = @_; push @{$self->{actions}}, $self->{info}{''} = SVK::Editor::InteractiveStatus::OpenDirectoryAction-> new(undef, $self, '', '', $baserev, $pool); return ''; } sub add_file { my ($self, $path, $pdir, $copy_path, $rev, $pool) = @_; push @{$self->{actions}}, $self->{info}{$path} = SVK::Editor::InteractiveStatus::AddFileAction->new($pdir, @_); return $path; } sub open_file { my ($self, $path, $pdir, $rev, $pool) = @_; push @{$self->{actions}}, $self->{info}{$path} = SVK::Editor::InteractiveStatus::ModifyFileAction->new($pdir, @_); return $path; } sub apply_textdelta { my ($self, $path, $checksum, $pool) = @_; my $action = $self->{info}{$path}; return $action->on_apply_textdelta(@_); } sub change_file_prop { my ($self, $path, $name, $value, $pool) = @_; push @{$self->{actions}}, $self->{info}{$path}{props}{$name} = SVK::Editor::InteractiveStatus::ModifyFilePropAction->new($path, @_); } sub close_file { my ($self, $path, $checksum, $pool) = @_; my $action = $self->{info}{$path}; $action->on_close_file(@_); } sub delete_entry { my ($self, $path, $rev, $pdir, $pool) = @_; my $parent = $path; do { $parent =~ s{/[^/]*$}{}; } while (not $self->{info}{$parent}); return if $pdir ne $parent; push @{$self->{actions}}, $self->{info}{$path} = SVK::Editor::InteractiveStatus::DeleteFileAction->new($parent, @_); } sub add_directory { my ($self, $path, $pdir, $copy_from, $rev, $pool) = @_; push @{$self->{actions}}, $self->{info}{$path} = SVK::Editor::InteractiveStatus::AddDirectoryAction->new($pdir, @_); return $path; } sub open_directory { my ($self, $path, $pdir, $rev, $pool) = @_; push @{$self->{actions}}, $self->{info}{$path} = SVK::Editor::InteractiveStatus::OpenDirectoryAction->new($pdir, @_); return $path; } sub change_dir_prop { my ($self, $path, $name, $value, $pool) = @_; push @{$self->{actions}}, $self->{info}{$path}{props}{$name} = SVK::Editor::InteractiveStatus::ModifyDirectoryPropAction->new($path, @_); } sub close_directory { my ($self, $path) = @_; my $action = $self->{info}{$path}; $action->on_close_directory(@_); } sub conflict { my ($self, $path) = @_; push @{$self->{conflicts}}, $path; } package SVK::Editor::InteractiveStatus::UI; use SVK::I18N; sub new { my ($class, $editor) = @_; my $qcount = 0; my $self = bless { actions => $editor->{actions}, current_question => -1, current_action => 0, current_action_question => -1 }, $class; $qcount += $_->get_questions_count for @{$self->{actions}}; $self->{questions_count} = $qcount; return $self; } sub _loc { my ($text, $keys, @keys) = @_; ($text, @keys) = split /#/, $text; return ($text, join("",@keys), $keys); } my %prompts = ( basic => [_loc("[a]ccept, [s]kip this change#a#s", "as")], fileChunks => [_loc("[A]ccept, [S]kip the rest of changes to this file#A#S", "AS")], fileProps => [_loc("a[c]cept, s[k]ip rest of changes to this file and its properties#c#k", "ck")], dirSubdir => [_loc("[A]ccept, [S]kip changes to whole subdirectory#A#S", "AS")], dirSubdirAcceptOnly => [_loc("[A]ccept changes to whole subdirectory#A", "A")], propsFile => [_loc("[A]ccept, [S]kip the rest of changes to this file properties#A#S", "AS")], propsDir => [_loc("[A]ccept, [S]kip the rest of changes to this directory properties#A#S", "AS")], props => [_loc("a[c]cept, s[k]ip changes to all properties with that name#c#k", "ck")], move_back => [_loc("move to [p]revious change#p", "p")], ); sub run { my $self = shift; my ($question, $flags, $state, $change_info, $action, $regexp); $self->next_question; while ($self->{current_question} < $self->{questions_count}) { my ($answers, $keys, @prompts); $action = $self->{actions}[$self->{current_action}]; ($question, $flags, $state, $change_info) = $action->get_question($self->{current_action_question}); my @flags = ('basic', @$flags); push @flags, 'move_back' if $self->{current_question} > 0; for (@flags) { push @prompts, $prompts{$_}->[0]; $keys.= $prompts{$_}->[1]; $answers.= $prompts{$_}->[2]; } $question = ($change_info||"")."\n[". ($self->{current_question}+1)."/$self->{questions_count}] ". "$question:\n".join(",\n", @prompts); if ($$state) { $regexp = qr/^[$keys]?$/; $question.= " [$$state]> "; } else { $regexp = qr/^[$keys]$/; $question.= " > "; } my $answer = SVK::Util::get_prompt($question, $regexp) || $$state; $answer =~ eval "\$answer =~ tr/$keys/$answers/"; if ($answer eq 'p') { $self->previous_question; next; } my $res = $action->on_state_update( $self->{current_action_question}, $answer); $$state = $answer; $self->update_questions_count if $res; $self->next_question; } } sub update_questions_count { my $self = shift; my $qcount = 0; $qcount += $_->get_questions_count for @{$self->{actions}}[0..$self->{current_action}-1]; $self->{current_question} = $self->{current_action_question} + $qcount; $qcount += $_->get_questions_count for @{$self->{actions}}[$self->{current_action}..$#{$self->{actions}}]; $self->{questions_count} = $qcount; } sub next_question { my $self = shift; my $qid = $self->{current_action_question}+1; my $aid = $self->{current_action}; $self->{current_question}++; do { if ($self->{actions}[$aid]->get_questions_count > $qid) { $self->{current_action_question} = $qid; $self->{current_action} = $aid; return 1; } $qid = 0; $aid++; } while ($aid < @{$self->{actions}}); return 0; } sub previous_question { my $self = shift; return 0 if $self->{current_question} == 0; if ($self->{current_action_question} > 0) { --$self->{current_action_question}; --$self->{current_question}; return 1; } my $aid = $self->{current_action}-1; $aid-- while $self->{actions}[$aid]->get_questions_count <= 0; $self->{current_action_question} = $self->{actions}[$aid]->get_questions_count - 1; $self->{current_action} = $aid; $self->{current_question}--; return 1; } package SVK::Editor::InteractiveStatus::Action; sub AUTOLOAD { no strict 'refs'; our $AUTOLOAD; my ($self, @arg) = @_; my $name = $AUTOLOAD; $name =~ s/^.*::on_(.*)_commit$/$1/; return unless $name and grep {$name eq $_} qw(delete_entry add_file open_file add_directory open_directory apply_textdelta change_file_prop close_file change_dir_prop close_directory); my $pos = SVK::Editor->baton_at($name); if ($pos >= 0) { *$AUTOLOAD = sub { my ($action, $editor, @args) = @_; $args[$pos] = $editor->{storage_baton}{$args[$pos]}; $editor->{storage}->$name(@args); } } else { *$AUTOLOAD = sub { my ($action, $editor, @args) = @_; $editor->{storage}->$name(@args); } } goto &$AUTOLOAD; } my %globalPropsState; my $currId = 0; sub new { my ($class, $parent, $editor, $path) = @_; my $self = bless { path => $path, id => $currId++, state => '', force_disable => {}, force_enable => {}, children => [], }, $class; $parent = $editor->{info}{$parent} if defined $parent; push @{$parent->{children}}, $self if $parent; return $self; } sub on_apply_textdelta { } sub on_close_file { } sub on_close_directory { } sub enabled { my $self = shift; return 0 if %{$self->{force_disable}}; return 1 if %{$self->{force_enable}}; return $self->{enabled} ? 1 : 0; } sub get_questions_count { my $self = shift; return %{$self->{force_disable}} || %{$self->{force_enable}} ? 0 : 1; } sub update_children_state { my ($self, $state, $by_who) = @_; return grep {$_->on_state_update(undef, $state, $by_who || $self)} @{$self->{children}}; } sub on_state_update { my ($self, $id, $state, $by_who) = @_; if ($by_who) { if ($state =~ /[Ac]/) { return 0 if exists $self->{force_enable}{$by_who->{id}}; delete $self->{force_disable}->{$by_who->{id}}; $self->{force_enable}{$by_who->{id}} = 1; } elsif ($state =~ /[Sk]/) { return 0 if exists $self->{force_disable}{$by_who->{id}}; delete $self->{force_enable}->{$by_who->{id}}; $self->{force_disable}{$by_who->{id}} = 1; } else { my $ret = delete $self->{force_enable}->{$by_who->{id}}; $ret ||= delete $self->{force_disable}->{$by_who->{id}}; return 0 if not $ret; $self->update_children_state($state, $by_who); return 1; } $self->update_children_state($state, $by_who); return 1; } $self->{enabled} = $state =~ /[aAc]/; return 0; } sub on_end_selection_phase { my ($self, $editor) = @_; $editor->{notify}->node_status($self->{path}, ''); } sub on_end_selection_phase_last_chance { my ($self, $editor) = @_; $editor->{notify}->flush($self->{path}); } package SVK::Editor::InteractiveStatus::AddFileAction; use base qw(SVK::Editor::InteractiveStatus::Action); use SVK::I18N; sub on_apply_textdelta_commit { my ($self, $editor, $path, $checksum, $pool) = @_; return $self->SUPER::on_apply_textdelta_commit(@_[1..$#_]) if $self->enabled; return undef; } sub get_question { my ($self, $id) = @_; return (loc("File '%1' is marked for addition", $self->{path}), [], \$self->{state}); } sub on_state_update { my ($self, $id, $state, $by_who) = @_; my $res = $self->SUPER::on_state_update($id, $state, $by_who); return $res if $by_who; return 0 if $state eq $self->{state}; return $self->update_children_state($state eq 's' ? 'S' : $state) if $self->{state} =~ /[s]/ or $state =~ /[s]/; return 0; } sub on_end_selection_phase { my ($self, $editor) = @_; if ($self->enabled) { $editor->{notify}->node_status($self->{path}, 'A'); } else { $editor->{notify}->node_status($self->{path}, ''); $editor->{cb_skip_add}($self->{path}); } } package SVK::Editor::InteractiveStatus::ModifyFileAction; use base qw(SVK::Editor::InteractiveStatus::Action); use List::Util qw(min max); use Digest::MD5 qw(md5_hex); use SVK::Util qw(mimetype_is_text); use SVK::I18N; sub new { my ($class, $parent, $editor, $path, $pdir, $rev, $pool) = @_; my $self = $class->SUPER::new(@_[1..$#_]); $self->{rev} = $rev; return $self } sub on_apply_textdelta { my ($self, $editor, $path, $checksum, $pool) = @_; my ($type) = grep {$_->{name} eq 'svn:mime-type'} @{$self->{children}}; if ($type and !mimetype_is_text($type->{value}) or ($type = $editor->{inspector}->localprop($path, 'svn:mime-type', $pool)) and !mimetype_is_text($type)) { bless $self, "SVK::Editor::InteractiveStatus::ModifyBinaryFileAction"; return $self->on_apply_textdelta(@_[1..$#_]); } my $fh1 = $editor->{inspector}->localmod($path, '', $pool)->[0]; { # XXX: some swig build doesn't like like mg $/ used in # SVN::Stream::readline, so we have to deal with the complicated last newline local $/; my $buf = <$fh1>; if (length $buf) { $self->{old_content} = [map { "$_\n" } $buf =~ m/^.*$/mg ]; substr($self->{old_content}[-1], -1, 1, '') if substr($buf, -1, 1) ne "\n"; } } $self->{new_content} = ''; open my $fh2, '>', \$self->{new_content}; $self->{original_checksum} = $checksum; return [SVN::TxDelta::apply($fh1, $fh2, undef, undef, $pool)]; } sub on_apply_textdelta_commit { my ($self, $editor, $path, $checksum, $pool) = @_; return undef if $self->{empty_change}; my $handle = SVK::Editor::InteractiveStatus::Action::on_apply_textdelta_commit(@_); return $handle if $self->{full_change}; if ($handle && $#{$handle} >= 0) { open my $nfh, '<', \$self->{new_content}; if ($editor->{send_fulltext}) { SVN::TxDelta::send_stream($nfh, @$handle, $self->{pool}); } else { my $ofh = $editor->{inspector}->localmod($path, '', $pool)->[0]; my $txstream = SVN::TxDelta::new($ofh, $nfh, $pool); SVN::TxDelta::send_txstream($txstream, @$handle, $self->{pool}); } } return undef; } sub on_close_file_commit { my ($self, $editor, $path, $checksum, $pool) = @_; $editor->{storage}->close_file($editor->{storage_baton}{$path}, $self->{checksum}, $pool); } sub enabled { my $self = shift; return 0 if %{$self->{force_disable}}; return 1 if %{$self->{force_enable}}; return 1 if grep {$_ =~ /[aAc]/} @{$self->{states}}; return grep {$_->enabled} @{$self->{children}}; } sub get_questions_count { my $self = shift; unless (exists $self->{chunks}) { $self->split_diff_into_chunks; } return $self->{cutoff}; } sub get_question { my ($self, $id) = @_; my @flags; push @flags, "fileChunks" if $id*2+1 < $#{$self->{chunks}}; push @flags, "fileProps" if @{$self->{children}}; return (loc("Modification to '%1' file", $self->{path}), \@flags, \$self->{states}[$id], $self->{chunks}[$id*2+1][2]); } sub on_state_update { my ($self, $id, $state, $by_who) = @_; my $res = $self->SUPER::on_state_update($id, $state, $by_who); return $res if $by_who; return 0 if $state eq $self->{states}[$id]; if ($self->{states}[$id] =~ /[ck]/ or $state =~ /[ck]/) { $res = $self->update_children_state($state =~ /[AS]/ ? 'a' : $state); } if ($state =~ /[ASck]/) { ++$id; return $self->{cutoff} = $id if $id != $self->{cutoff}; return $res; } $id = $self->{cutoff}; $self->{cutoff} = int(@{$self->{chunks}}/2); return $id ne $self->{cutoff}; } use constant CONTEXT => 5; sub split_diff_into_chunks { my $self = shift; my ($p, $b1, $b2, $d1, $d2, $i1, $i2, $a1, $a2) = (0); my $di; my @old_content = $self->{old_content} ? @{$self->{old_content}} : (); delete $self->{old_content}; my @new_content = defined $self->{new_content} ? $self->{new_content} =~ m/.*\n?/g : (1); pop @new_content; my ($lln, $rln) = ( !(@old_content and $old_content[-1] =~ /\n$/), !(@new_content and $new_content[-1] =~ /\n$/)); my ($lo, $ln) = (@old_content-1, @new_content-1); my $diff = new Algorithm::Diff(\@old_content, \@new_content); while ($diff->Next()) { next if $diff->Same(); ($d1, $d2, $i1, $i2) = $diff->Get(qw(Min1 Max1 Min2 Max2)); if (not $diff->Items(2)) { ($b1, $b2) = (max(0, $d1 - CONTEXT), max(-1, $d1 - 1)); ($a1, $a2) = (min($lo + 1, $d2 + 1), min($lo, $d2 + CONTEXT)); } elsif (not $diff->Items(1)) { ($b1, $b2) = (max(0, $d2 - CONTEXT - 1), $d2); ($a1, $a2) = (min($lo + 1, $b2 + 1), min($lo, $b2 + CONTEXT)); } else { ($b1, $b2) = (max(0, $d1 - CONTEXT), max(-1, $d1 - 1)); ($a1, $a2) = (min($lo + 1, $d2 + 1), min($lo, $d2 + CONTEXT)); } $di = "--- $self->{path}\t(revision $self->{rev})\n"; $di.= "+++ $self->{path}\t(local)\n"; my ($l1, $l2) = map {$_ < 1 ? "" : ",$_" } $a2-$b1, $i2-$i1+$a2-$b1; $di.= "@@ -$b1$l1 +@{[$i1+$b1-$d1]}$l2 @@\n"; $di.= join " ","",@old_content[$b1..$b2] if $b1 <= $b2; if ($d1 <= $d2) { $di.= join "-","",@old_content[$d1..$d2]; $di.= "\n\\ No newline at end of file\n" if $lln and $d2 == $lo; } if ($i1 <= $i2) { $di.= join "+","",@new_content[$i1..$i2]; $di.= "\n\\ No newline at end of file\n" if $rln and $i2 == $ln; } $di.= join " ","",@old_content[$a1..$a2] if $a1 <= $a2; my $b = $p <= $b2 ? join "",@old_content[$p..$b2] : ""; my $d = $d1 <= $d2 ? join "",@old_content[$d1..$d2] : ""; my $i = $i1 <= $i2 ? join "",@new_content[$i1..$i2] : ""; $p = $a1; push @{$self->{chunks}}, $b, [$d, $i, $di]; } push(@{$self->{chunks}}, join("",@old_content[$p..$lo])); $self->{cutoff} = int(@{$self->{chunks}}/2); $self->{states} = [('') x $self->{cutoff}]; } sub on_end_selection_phase { my ($self, $editor) = @_; unless (exists $self->{chunks}) { $self->split_diff_into_chunks; } my @states = map {$_ =~ /[aAc]/ ? 1 : 0} @{$self->{states}}; @states[$self->{cutoff}..@states] = ($self->{cutoff} ? $states[$self->{cutoff}-1] : '') x (@states - $self->{cutoff}); unless (grep {$_} @states) { $editor->{notify}->node_status($self->{path}, ''); return $self->{empty_change} = 1; } $editor->{notify}->node_status($self->{path}, 'M'); return $self->{full_change} = 1 unless grep {!$_} @states; my $v = ''; for (my $idx = 1; $idx < @{$self->{chunks}}; $idx += 2) { $v.= $self->{chunks}[$idx-1]; $v.= $self->{chunks}[$idx][$states[$idx/2]]; } $self->{new_content} = $v.$self->{chunks}[-1]; $self->{checksum} = md5_hex($self->{new_content}); } package SVK::Editor::InteractiveStatus::ModifyBinaryFileAction; use base qw(SVK::Editor::InteractiveStatus::AddFileAction); use SVK::I18N; sub get_question { my ($self, $id) = @_; my @flags; push @flags, "fileProps" if @{$self->{children}}; return (loc("Modifications to binary file '%1'", $self->{path}), \@flags, \$self->{state}); } sub on_state_update { my ($self, $id, $state, $by_who) = @_; my $res = $self->SUPER::on_state_update($id, $state, $by_who); return $res if $by_who; if ($state =~ /[ck]/ or $self->{state} =~ /[ck]/) { return $self->update_children_state($state); } return 0; } sub on_end_selection_phase { my ($self, $editor) = @_; $editor->{notify}->node_status($self->{path}, $self->enabled ? 'U' : ''); } package SVK::Editor::InteractiveStatus::DeleteFileAction; use base qw(SVK::Editor::InteractiveStatus::Action); use SVK::I18N; sub get_question { my ($self, $id) = @_; return (loc("File or directory '%1' is marked for deletion", $self->{path}), [], \$self->{state}); } sub on_end_selection_phase { my ($self, $editor) = @_; $editor->{notify}->node_status($self->{path}, 'D') if $self->{enabled} } package SVK::Editor::InteractiveStatus::ModifyFilePropAction; use base qw(SVK::Editor::InteractiveStatus::Action); use SVK::I18N; use SVK::Util qw(tmpfile slurp_fh); sub new { my ($class, $parent, $editor, $path, $name, $value, $pool) = @_; my $self = $class->SUPER::new(@_[1..$#_]); my ($l, $r) = $editor->{inspector}->localprop($path, $name, $pool); ($l, $r) = map { !length || /\n$/ ? $_ : "$_\n"} defined $l ? $l : "", defined $value ? $value : ""; my ($lfh, $lfn) = tmpfile('diff'); my ($rfh, $rfn) = tmpfile('diff'); slurp_fh($l, $lfh); close($lfh); slurp_fh($r, $rfh); close($rfh); my $diff = ''; open my $fh, '>', \$diff; my $dh = SVN::Core::diff_file_diff($lfn, $rfn); SVN::Core::diff_file_output_unified($fh, $dh, $lfn, $rfn, '', '', $pool); $diff =~ s/.*\n.*\n//; $diff =~ s/^\@.*\n//mg; $diff =~ s/^/ /mg; $self->{name} = $name; $self->{value} = $value; $self->{diff} = "Property change on $path\n".("_" x 67)."\n". "Name: $name\n$diff"; return $self; } sub get_questions_count { my $self = shift; return %{$self->{force_disable}} || %{$self->{force_enable}} || exists $globalPropsState{$self->{name}} && $self->{state} !~ /[ck]/ ? 0 : 1; } sub get_question { my ($self, $id) = @_; my @flags = qw(props); # unshift @flags, "propsFile" if $id*2 < @{$self->{chunks}}; return (loc("Property change on '%1' file requested", $self->{path}), \@flags, \$self->{state}, $self->{diff}); } sub on_state_update { my ($self, $id, $state, $by_who) = @_; my $res = $self->SUPER::on_state_update($id, $state, $by_who); return $res if $by_who; if ($state ne $self->{state}) { if ($state eq 'c') { $globalPropsState{$self->{name}} = 1; } elsif ($state eq 'k') { $globalPropsState{$self->{name}} = 0; } elsif ($self->{state} =~ /[ck]/) { delete $globalPropsState{$self->{name}}; } else { return 0; } return 1; } return 0; } sub enabled { my $self = shift; my $globPropState = $globalPropsState{$self->{name}}; return 0 if %{$self->{force_disable}} or defined $globPropState and not $globPropState; return 1 if %{$self->{force_enable}} or $globPropState; return $self->{enabled}; } sub on_end_selection_phase { my ($self, $editor) = @_; if ($self->enabled) { $editor->{notify}->prop_status($self->{path}, 'M'); } else { $editor->{cb_skip_prop_change}($self->{path}, $self->{name}, $self->{value}); } } sub on_end_selection_phase_last_chance { } package SVK::Editor::InteractiveStatus::AddDirectoryAction; use base qw(SVK::Editor::InteractiveStatus::Action); use SVK::I18N; sub get_question { my ($self, $id) = @_; my @flags = (); push @flags, "dirSubdirAcceptOnly" if grep { (ref $_) =~ '::Add(?:File|Directory)Action$' } @{$self->{children}}; return (loc("Directory '%1' is marked for addition", $self->{path}), \@flags, \$self->{state}); } sub on_state_update { my ($self, $id, $state, $by_who) = @_; my $res = $self->SUPER::on_state_update($id, $state, $by_who); return $res if $by_who; return $self->update_children_state($state eq 's' ? 'S' : $state) if $state ne $self->{state} and ($self->{state} or $state =~ /[sA]/); return 0; } sub on_end_selection_phase { my ($self, $editor) = @_; if ($self->enabled) { $editor->{notify}->node_status($self->{path}, 'A'); } else { $editor->{notify}->node_status($self->{path}, ''); $editor->{cb_skip_add}($self->{path}); } } sub on_end_selection_phase_last_chance { my ($self, $editor) = @_; $editor->{notify}->flush($self->{path}); } package SVK::Editor::InteractiveStatus::OpenDirectoryAction; use base qw(SVK::Editor::InteractiveStatus::Action); sub get_questions_count { return 0; } package SVK::Editor::InteractiveStatus::ModifyDirectoryPropAction; use base qw(SVK::Editor::InteractiveStatus::ModifyFilePropAction); use SVK::I18N; sub get_question { my ($self, $id) = @_; my @flags = qw(props); my $path = $self->{path} eq '' ? "." : "$self->{path}"; # unshift @flags, "propsFile" if $id*2 < @{$self->{chunks}}; return (loc("Property change on '%1' directory requested", $path), \@flags, \$self->{state}, $self->{diff}); } 1;