summaryrefslogtreecommitdiffstats
path: root/Bugzilla/PatchReader/AddCVSContext.pm
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla/PatchReader/AddCVSContext.pm')
-rw-r--r--Bugzilla/PatchReader/AddCVSContext.pm226
1 files changed, 226 insertions, 0 deletions
diff --git a/Bugzilla/PatchReader/AddCVSContext.pm b/Bugzilla/PatchReader/AddCVSContext.pm
new file mode 100644
index 000000000..910e45669
--- /dev/null
+++ b/Bugzilla/PatchReader/AddCVSContext.pm
@@ -0,0 +1,226 @@
+package Bugzilla::PatchReader::AddCVSContext;
+
+use Bugzilla::PatchReader::FilterPatch;
+use Bugzilla::PatchReader::CVSClient;
+use Cwd;
+use File::Temp;
+
+use strict;
+
+@Bugzilla::PatchReader::AddCVSContext::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+
+# XXX If you need to, get the entire patch worth of files and do a single
+# cvs update of all files as soon as you find a file where you need to do a
+# cvs update, to avoid the significant connect overhead
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ $this->{CONTEXT} = $_[0];
+ $this->{CVSROOT} = $_[1];
+
+ return $this;
+}
+
+sub my_rmtree {
+ my ($this, $dir) = @_;
+ foreach my $file (glob("$dir/*")) {
+ if (-d $file) {
+ $this->my_rmtree($file);
+ } else {
+ trick_taint($file);
+ unlink $file;
+ }
+ }
+ trick_taint($dir);
+ rmdir $dir;
+}
+
+sub end_patch {
+ my $this = shift;
+ if (exists($this->{TMPDIR})) {
+ # Set as variable to get rid of taint
+ # One would like to use rmtree here, but that is not taint-safe.
+ $this->my_rmtree($this->{TMPDIR});
+ }
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+ $this->{HAS_CVS_CONTEXT} = !$file->{is_add} && !$file->{is_remove} &&
+ $file->{old_revision};
+ $this->{REVISION} = $file->{old_revision};
+ $this->{FILENAME} = $file->{filename};
+ $this->{SECTION_END} = -1;
+ $this->{TARGET}->start_file(@_) if $this->{TARGET};
+}
+
+sub end_file {
+ my $this = shift;
+ $this->flush_section();
+
+ if ($this->{FILE}) {
+ close $this->{FILE};
+ unlink $this->{FILE}; # If it fails, it fails ...
+ delete $this->{FILE};
+ }
+ $this->{TARGET}->end_file(@_) if $this->{TARGET};
+}
+
+sub next_section {
+ my $this = shift;
+ my ($section) = @_;
+ $this->{NEXT_PATCH_LINE} = $section->{old_start};
+ $this->{NEXT_NEW_LINE} = $section->{new_start};
+ foreach my $line (@{$section->{lines}}) {
+ # If this is a line requiring context ...
+ if ($line =~ /^[-\+]/) {
+ # Determine how much context is needed for both the previous section line
+ # and this one:
+ # - If there is no old line, start new section
+ # - If this is file context, add (old section end to new line) context to
+ # the existing section
+ # - If old end context line + 1 < new start context line, there is an empty
+ # space and therefore we end the old section and start the new one
+ # - Else we add (old start context line through new line) context to
+ # existing section
+ if (! exists($this->{SECTION})) {
+ $this->_start_section();
+ } elsif ($this->{CONTEXT} eq "file") {
+ $this->push_context_lines($this->{SECTION_END} + 1,
+ $this->{NEXT_PATCH_LINE} - 1);
+ } else {
+ my $start_context = $this->{NEXT_PATCH_LINE} - $this->{CONTEXT};
+ $start_context = $start_context > 0 ? $start_context : 0;
+ if (($this->{SECTION_END} + $this->{CONTEXT} + 1) < $start_context) {
+ $this->flush_section();
+ $this->_start_section();
+ } else {
+ $this->push_context_lines($this->{SECTION_END} + 1,
+ $this->{NEXT_PATCH_LINE} - 1);
+ }
+ }
+ push @{$this->{SECTION}{lines}}, $line;
+ if (substr($line, 0, 1) eq "+") {
+ $this->{SECTION}{plus_lines}++;
+ $this->{SECTION}{new_lines}++;
+ $this->{NEXT_NEW_LINE}++;
+ } else {
+ $this->{SECTION_END}++;
+ $this->{SECTION}{minus_lines}++;
+ $this->{SECTION}{old_lines}++;
+ $this->{NEXT_PATCH_LINE}++;
+ }
+ } else {
+ $this->{NEXT_PATCH_LINE}++;
+ $this->{NEXT_NEW_LINE}++;
+ }
+ # If this is context, for now lose it (later we should try and determine if
+ # we can just use it instead of pulling the file all the time)
+ }
+}
+
+sub determine_start {
+ my ($this, $line) = @_;
+ return 0 if $line < 0;
+ if ($this->{CONTEXT} eq "file") {
+ return 1;
+ } else {
+ my $start = $line - $this->{CONTEXT};
+ $start = $start > 0 ? $start : 1;
+ return $start;
+ }
+}
+
+sub _start_section {
+ my $this = shift;
+
+ # Add the context to the beginning
+ $this->{SECTION}{old_start} = $this->determine_start($this->{NEXT_PATCH_LINE});
+ $this->{SECTION}{new_start} = $this->determine_start($this->{NEXT_NEW_LINE});
+ $this->{SECTION}{old_lines} = 0;
+ $this->{SECTION}{new_lines} = 0;
+ $this->{SECTION}{minus_lines} = 0;
+ $this->{SECTION}{plus_lines} = 0;
+ $this->{SECTION_END} = $this->{SECTION}{old_start} - 1;
+ $this->push_context_lines($this->{SECTION}{old_start},
+ $this->{NEXT_PATCH_LINE} - 1);
+}
+
+sub flush_section {
+ my $this = shift;
+
+ if ($this->{SECTION}) {
+ # Add the necessary context to the end
+ if ($this->{CONTEXT} eq "file") {
+ $this->push_context_lines($this->{SECTION_END} + 1, "file");
+ } else {
+ $this->push_context_lines($this->{SECTION_END} + 1,
+ $this->{SECTION_END} + $this->{CONTEXT});
+ }
+ # Send the section and line notifications
+ $this->{TARGET}->next_section($this->{SECTION}) if $this->{TARGET};
+ delete $this->{SECTION};
+ $this->{SECTION_END} = 0;
+ }
+}
+
+sub push_context_lines {
+ my $this = shift;
+ # Grab from start to end
+ my ($start, $end) = @_;
+ return if $end ne "file" && $start > $end;
+
+ # If it's an added / removed file, don't do anything
+ return if ! $this->{HAS_CVS_CONTEXT};
+
+ # Get and open the file if necessary
+ if (!$this->{FILE}) {
+ my $olddir = getcwd();
+ if (! exists($this->{TMPDIR})) {
+ $this->{TMPDIR} = File::Temp::tempdir();
+ if (! -d $this->{TMPDIR}) {
+ die "Could not get temporary directory";
+ }
+ }
+ chdir($this->{TMPDIR}) or die "Could not cd $this->{TMPDIR}";
+ if (Bugzilla::PatchReader::CVSClient::cvs_co_rev($this->{CVSROOT}, $this->{REVISION}, $this->{FILENAME})) {
+ die "Could not check out $this->{FILENAME} r$this->{REVISION} from $this->{CVSROOT}";
+ }
+ open my $fh, $this->{FILENAME} or die "Could not open $this->{FILENAME}";
+ $this->{FILE} = $fh;
+ $this->{NEXT_FILE_LINE} = 1;
+ trick_taint($olddir); # $olddir comes from getcwd()
+ chdir($olddir) or die "Could not cd back to $olddir";
+ }
+
+ # Read through the file to reach the line we need
+ die "File read too far!" if $this->{NEXT_FILE_LINE} && $this->{NEXT_FILE_LINE} > $start;
+ my $fh = $this->{FILE};
+ while ($this->{NEXT_FILE_LINE} < $start) {
+ my $dummy = <$fh>;
+ $this->{NEXT_FILE_LINE}++;
+ }
+ my $i = $start;
+ for (; $end eq "file" || $i <= $end; $i++) {
+ my $line = <$fh>;
+ last if !defined($line);
+ $line =~ s/\r\n/\n/g;
+ push @{$this->{SECTION}{lines}}, " $line";
+ $this->{NEXT_FILE_LINE}++;
+ $this->{SECTION}{old_lines}++;
+ $this->{SECTION}{new_lines}++;
+ }
+ $this->{SECTION_END} = $i - 1;
+}
+
+sub trick_taint {
+ $_[0] =~ /^(.*)$/s;
+ $_[0] = $1;
+ return (defined($_[0]));
+}
+
+1;