1package SVN::Notify::HTML::ColorDiff;
2
3use strict;
4use HTML::Entities;
5use SVN::Notify::HTML ();
6
7$SVN::Notify::HTML::ColorDiff::VERSION = '2.87';
8@SVN::Notify::HTML::ColorDiff::ISA = qw(SVN::Notify::HTML);
9
10=head1 Name
11
12SVN::Notify::HTML::ColorDiff - Subversion activity HTML notification with colorized diff
13
14=head1 Synopsis
15
16Use F<svnnotify> in F<post-commit>:
17
18  svnnotify --repos-path "$1" --revision "$2" \
19    --to developers@example.com --handler HTML::ColorDiff [options]
20
21Use the class in a custom script:
22
23  use SVN::Notify::HTML::ColorDiff;
24
25  my $notifier = SVN::Notify::HTML::ColorDiff->new(%params);
26  $notifier->prepare;
27  $notifier->execute;
28
29=head1 Description
30
31This subclass of L<SVN::Notify::HTML|SVN::Notify::HTML> sends HTML formatted
32email messages for Subversion activity, and if the C<with_diff> parameter is
33specified (but not C<attach_diff>), then a pretty colorized version of the
34diff will be included, rather than the plain text diff output by
35SVN::Notify::HTML.
36
37=head1 Usage
38
39To use SVN::Notify::HTML::ColorDiff, simply follow the
40L<instructions|SVN::Notify/Usage> in SVN::Notify, but when using F<svnnotify>,
41specify C<--handler HTML::ColorDiff>.
42
43=cut
44
45##############################################################################
46
47=head1 Instance Interface
48
49=head2 Instance Methods
50
51=head3 output_css
52
53  $notifier->output_css($file_handle);
54
55This method starts outputs the CSS for the HTML message.
56SVN::Notify::HTML::ColorDiff adds extra CSS to its output so that it can
57nicely style the diff.
58
59=cut
60
61# We use _css() so that ColorDiff can override it and the filters then applied
62# only one to all of the CSS.
63
64##############################################################################
65
66=head3 output_diff
67
68  $notifier->output_diff($out_file_handle, $diff_file_handle);
69
70Reads the diff data from C<$diff_file_handle> and prints it to
71C<$out_file_handle> for inclusion in the notification message. The diff is
72output with nice colorized HTML markup. Each line of the diff file is escaped
73by C<HTML::Entities::encode_entities()>.
74
75If there are any C<diff> filters, this method will do no HTML formatting, but
76redispatch to L<SVN::Notify::output_diff|SVN::Notify/"output_diff">. See
77L<Writing Output Filters|SVN::Notify/"Writing Output Filters"> for details on
78filters.
79
80=cut
81
82my %types = (
83    Modified => 'modfile',
84    Added    => 'addfile',
85    Deleted  => 'delfile',
86    Copied   => 'copfile',
87);
88
89sub output_diff {
90    my ($self, $out, $diff) = @_;
91    if ( $self->filters_for('diff') ) {
92        return $self->SUPER::output_diff($out, $diff);
93    }
94    $self->_dbpnt( "Outputting colorized HTML diff") if $self->verbose > 1;
95
96    my $in_div;
97    my $in_span = '';
98    print $out qq{</div>\n<div id="patch">\n<h3>Diff</h3>\n};
99    my ($length, %seen) = 0;
100    my $max = $self->max_diff_length;
101
102    while (my $line = <$diff>) {
103        $line =~ s/[\n\r]+$//;
104        next unless $line;
105        if ( $max && ( $length += length $line ) >= $max ) {
106            print $out "</$in_span>" if $in_span;
107            print $out qq{<span class="lines">\@\@ Diff output truncated at $max characters. \@\@\n</span>};
108            $in_span = '';
109            last;
110        } else {
111            if ($line =~ /^(Modified|Added|Deleted|Copied): (.*)/) {
112                my $class = $types{my $action = $1};
113                ++$seen{$2};
114                my $file = encode_entities($2, '<>&"');
115                (my $id = $file) =~ s/[^\w_]//g;
116
117                print $out "</$in_span>" if $in_span;
118                print $out "</span></pre></div>\n" if $in_div;
119
120                # Dump line, but check it's content.
121                if (<$diff> !~ /^=/) {
122                    # Looks like they used --no-diff-added or --no-diff-deleted.
123                    ($in_span, $in_div) = '';
124                    print $out qq{<a id="$id"></a>\n<div class="$class">},
125                        qq{<h4>$action: $file</h4></div>\n};
126                    next;
127                }
128
129                # Get the revision numbers.
130                my $before = <$diff>;
131                $before =~ s/[\n\r]+$//;
132
133                if ($before =~ /^\(Binary files differ\)/) {
134                    # Just output the whole file div.
135                    print $out qq{<a id="$id"></a>\n<div class="binary"><h4>},
136                      qq{$action: $file</h4>\n<pre class="diff"><span>\n},
137                      qq{<span class="cx">$before\n</span></span></pre></div>\n};
138                    ($in_span, $in_div) = '';
139                    next;
140                }
141
142                my ($rev1) = $before =~ /\(rev (\d+)\)$/;
143                my $after = <$diff>;
144                $after =~ s/[\n\r]+$//;
145                my ($rev2) = $after =~ /\(rev (\d+)\)$/;
146
147                # Output the headers.
148                print $out qq{<a id="$id"></a>\n<div class="$class"><h4>$action: $file},
149                  " ($rev1 => $rev2)</h4>\n";
150                print $out qq{<pre class="diff"><span>\n<span class="info">};
151                $in_div = 1;
152                print $out encode_entities($_, '<>&"'), "\n" for ($before, $after);
153                print $out "</span>";
154                $in_span = '';
155            } elsif ($line =~ /^Property changes on: (.*)/ && !$seen{$1}) {
156                # It's just property changes.
157                my $file = encode_entities($1, '<>&"');
158                (my $id = $file) =~ s/[^\w_]//g;
159                # Dump line.
160                <$diff>;
161
162                # Output the headers.
163                print $out "</$in_span>" if $in_span;
164                print $out "</span></pre></div>\n" if $in_div;
165                print $out qq{<a id="$id"></a>\n<div class="propset">},
166                  qq{<h4>Property changes: $file</h4>\n<pre class="diff"><span>\n};
167                $in_div = 1;
168                $in_span = '';
169            } elsif ($line =~ /^\@\@/) {
170                print $out "</$in_span>" if $in_span;
171                print $out (
172                    qq{<span class="lines">},
173                    encode_entities($line, '<>&"'),
174                    "\n</span>",
175                );
176                $in_span = '';
177            } elsif ($line =~ /^([-+])/) {
178                my $type = $1 eq '+' ? 'ins' : 'del';
179                if ($in_span eq $type) {
180                    print $out encode_entities($line, '<>&"'), "\n";
181                } else {
182                    print $out "</$in_span>" if $in_span;
183                    print $out (
184                        qq{<$type>},
185                        encode_entities($line, '<>&"'),
186                        "\n",
187                    );
188                    $in_span = $type;
189                }
190            } else {
191                if ($in_span eq 'cx') {
192                    print $out encode_entities($line, '<>&"'), "\n";
193                } else {
194                    print $out "</$in_span>" if $in_span;
195                    print $out (
196                        qq{<span class="cx">},
197                        encode_entities($line, '<>&"'),
198                        "\n",
199                    );
200                    $in_span = 'span';
201                }
202            }
203        }
204    }
205    print $out "</$in_span>" if $in_span;
206    print $out "</span></pre>\n</div>\n" if $in_div;
207    print $out "</div>\n";
208
209    close $diff or warn "Child process exited: $?\n";
210    return $self;
211}
212
213##############################################################################
214
215sub _css {
216    my $css = shift->SUPER::_css;
217    push @$css,
218        qq(#patch h4 {font-family: verdana,arial,helvetica,sans-serif;),
219            qq(font-size:10pt;padding:8px;background:#369;color:#fff;),
220            qq(margin:0;}\n),
221        qq(#patch .propset h4, #patch .binary h4 {margin:0;}\n),
222         qq(#patch pre {padding:0;line-height:1.2em;margin:0;}\n),
223        qq(#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;),
224            qq(overflow:auto;}\n),
225        qq(#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}\n),
226        qq(#patch span {display:block;padding:0 10px;}\n),
227        qq(#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, ),
228            qq(#patch .binary, #patch .copfile {border:1px solid #ccc;),
229            qq(margin:10px 0;}\n),
230        qq(#patch ins {background:#dfd;text-decoration:none;display:block;),
231            qq(padding:0 10px;}\n),
232        qq(#patch del {background:#fdd;text-decoration:none;display:block;),
233            qq(padding:0 10px;}\n),
234        qq(#patch .lines, .info {color:#888;background:#fff;}\n);
235    return $css;
236}
237
2381;
239__END__
240
241=head1 See Also
242
243=over
244
245=item L<SVN::Notify|SVN::Notify>
246
247=item L<SVN::Notify::HTML|SVN::Notify::HTML>
248
249=item L<CVSspam|http://www.badgers-in-foil.co.uk/projects/cvsspam/>
250
251=back
252
253=head1 To Do
254
255=over
256
257=item *
258
259Add inline emphasis just on the text that changed between two lines, like
260this: L<http://www.badgers-in-foil.co.uk/projects/cvsspam/example.html>.
261
262=item *
263
264Add links to To Do stuff to the top of the email, as pulled in from the diff.
265This might be tricky, since the diff is currently output I<after> the message
266body. Maybe use absolute positioning CSS?
267
268=back
269
270=head1
271
272=head1 Author
273
274David E. Wheeler <david@justatheory.com>
275
276=head1 Copyright and License
277
278Copyright (c) 2004-2018 David E. Wheeler. Some Rights Reserved.
279
280This module is free software; you can redistribute it and/or modify it under
281the same terms as Perl itself.
282
283=cut
284