Compare two issues of 'what's cooking'

Junio C Hamano committed Jan 4, 2010 at 13:55 UTC 3f9558ac5f6ab9a6e2d2937ae12591b06568170e
1 file changed +293
compare-cooking.perl new
+293
@@ -0,0 +1,293 @@
1 +#!/usr/bin/perl -w
2 +
3 +my ($old, $new);
4 +
5 +if (@ARGV == 7) {
6 + # called as GIT_EXTERNAL_DIFF script
7 + $old = parse_cooking($ARGV[1]);
8 + $new = parse_cooking($ARGV[4]);
9 +} else {
10 + # called with old and new
11 + $old = parse_cooking($ARGV[0]);
12 + $new = parse_cooking($ARGV[1]);
13 +}
14 +compare_cooking($old, $new);
15 +
16 +################################################################
17 +
18 +use File::Temp qw(tempfile);
19 +
20 +sub compare_them {
21 + local($_);
22 + my ($a, $b, $force, $soft) = @_;
23 +
24 + if ($soft) {
25 + $plus = $minus = ' ';
26 + } else {
27 + $plus = '+';
28 + $minus = '-';
29 + }
30 +
31 + if (!defined $a->[0]) {
32 + return map { "$plus$_\n" } map { split(/\n/) } @{$b};
33 + } elsif (!defined $b->[0]) {
34 + return map { "$minus$_\n" } map { split(/\n/) } @{$a};
35 + } elsif (join('', @$a) eq join('', @$b)) {
36 + if ($force) {
37 + return map { " $_\n" } map { split(/\n/) } @{$a};
38 + } else {
39 + return ();
40 + }
41 + }
42 + my ($ah, $aname) = tempfile();
43 + my ($bh, $bname) = tempfile();
44 + my $cnt = 0;
45 + my @result = ();
46 + for (@$a) {
47 + print $ah $_;
48 + $cnt += tr/\n/\n/;
49 + }
50 + for (@$b) {
51 + print $bh $_;
52 + $cnt += tr/\n/\n/;
53 + }
54 + close $ah;
55 + close $bh;
56 + open(my $fh, "-|", 'diff', "-U$cnt", $aname, $bname);
57 + $cnt = 0;
58 + while (<$fh>) {
59 + next if ($cnt++ < 3);
60 + push @result, $_;
61 + }
62 + close $fh;
63 + unlink ($aname, $bname);
64 + return @result;
65 +}
66 +
67 +sub flush_topic {
68 + my ($cooking, $name, $desc) = @_;
69 + my $section = $cooking->{SECTIONS}[-1];
70 +
71 + return if (!defined $name);
72 +
73 + $desc =~ s/\s+\Z/\n/s;
74 + $desc =~ s/\A\s+//s;
75 + my $topic = +{
76 + IN_SECTION => $section,
77 + NAME => $name,
78 + DESC => $desc,
79 + };
80 + $cooking->{TOPICS}{$name} = $topic;
81 + push @{$cooking->{TOPIC_ORDER}}, $name;
82 +}
83 +
84 +sub parse_section {
85 + my ($cooking, @line) = @_;
86 +
87 + while (@line && $line[-1] =~ /^\s*$/) {
88 + pop @line;
89 + }
90 + return if (!@line);
91 +
92 + if (!exists $cooking->{SECTIONS}) {
93 + $cooking->{SECTIONS} = [];
94 + $cooking->{TOPICS} = {};
95 + $cooking->{TOPIC_ORDER} = [];
96 + }
97 + if (!exists $cooking->{HEADER}) {
98 + my $line = join('', @line);
99 + $line =~ s/\A.*?\n\n//s;
100 + $cooking->{HEADER} = $line;
101 + return;
102 + }
103 + if (!exists $cooking->{GREETING}) {
104 + $cooking->{GREETING} = join('', @line);
105 + return;
106 + }
107 +
108 + my ($section_name, $topic_name, $topic_desc);
109 + for (@line) {
110 + if (!defined $section_name && /^\[(.*)\]$/) {
111 + $section_name = $1;
112 + push @{$cooking->{SECTIONS}}, $section_name;
113 + next;
114 + }
115 + if (/^\* (\S+) /) {
116 + my $next_name = $1;
117 + flush_topic($cooking, $topic_name, $topic_desc);
118 + $topic_name = $next_name;
119 + $topic_desc = '';
120 + }
121 + $topic_desc .= $_;
122 + }
123 + flush_topic($cooking, $topic_name, $topic_desc);
124 +}
125 +
126 +sub dump_cooking {
127 + my ($cooking) = @_;
128 + print $cooking->{HEADER};
129 + print "-" x 50, "\n";
130 + print $cooking->{GREETING};
131 + for my $section_name (@{$cooking->{SECTIONS}}) {
132 + print "\n", "-" x 50, "\n";
133 + print "[$section_name]\n";
134 + for my $topic_name (@{$cooking->{TOPIC_ORDER}}) {
135 + $topic = $cooking->{TOPICS}{$topic_name};
136 + next if ($topic->{IN_SECTION} ne $section_name);
137 + print "\n", $topic->{DESC};
138 + }
139 + }
140 +}
141 +
142 +sub parse_cooking {
143 + my ($filename) = @_;
144 + my (%cooking, @current, $fh);
145 + open $fh, "<", $filename
146 + or die "cannot open $filename: $!";
147 + while (<$fh>) {
148 + if (/^-{30,}$/) {
149 + parse_section(\%cooking, @current);
150 + @current = ();
151 + next;
152 + }
153 + push @current, $_;
154 + }
155 + close $fh;
156 + parse_section(\%cooking, @current);
157 +
158 + return \%cooking;
159 +}
160 +
161 +sub compare_topics {
162 + my ($a, $b) = @_;
163 + if (!@$a || !@$b) {
164 + print compare_them($a, $b, 1, 1);
165 + return;
166 + }
167 +
168 + # otherwise they both have title.
169 + $a = [map { "$_\n" } split(/\n/, join('', @$a))];
170 + $b = [map { "$_\n" } split(/\n/, join('', @$b))];
171 + my $atitle = shift @$a;
172 + my $btitle = shift @$b;
173 + print compare_them([$atitle], [$btitle], 1);
174 +
175 + my (@atail, @btail);
176 + while (@$a && $a->[-1] !~ /^\s/) {
177 + unshift @atail, pop @$a;
178 + }
179 + while (@$b && $b->[-1] !~ /^\s/) {
180 + unshift @btail, pop @$b;
181 + }
182 + print compare_them($a, $b);
183 + print compare_them(\@atail, \@btail);
184 +}
185 +
186 +sub compare_class {
187 + my ($fromto, $names, $topics) = @_;
188 +
189 + my (@where, %where);
190 + for my $name (@$names) {
191 + my $t = $topics->{$name};
192 + my ($a, $b, $in, $force);
193 + if ($t->{OLD} && $t->{NEW}) {
194 + $a = [$t->{OLD}{DESC}];
195 + $b = [$t->{NEW}{DESC}];
196 + if ($t->{OLD}{IN_SECTION} ne $t->{NEW}{IN_SECTION}) {
197 + $force = 1;
198 + $in = '';
199 + } else {
200 + $in = "[$t->{NEW}{IN_SECTION}]";
201 + }
202 + } elsif ($t->{OLD}) {
203 + $a = [$t->{OLD}{DESC}];
204 + $b = [];
205 + $in = "Was in [$t->{OLD}{IN_SECTION}]";
206 + } else {
207 + $a = [];
208 + $b = [$t->{NEW}{DESC}];
209 + $in = "[$t->{NEW}{IN_SECTION}]";
210 + }
211 + next if (defined $a->[0] &&
212 + defined $b->[0] &&
213 + $a->[0] eq $b->[0] && !$force);
214 +
215 + if (!exists $where{$in}) {
216 + push @where, $in;
217 + $where{$in} = [];
218 + }
219 + push @{$where{$in}}, [$a, $b];
220 + }
221 +
222 + return if (!@where);
223 + for my $in (@where) {
224 + my @bag = @{$where{$in}};
225 + if (defined $fromto && $fromto ne '') {
226 + print "\n", '-' x 50, "\n$fromto\n";
227 + $fromto = undef;
228 + }
229 + print "\n$in\n" if ($in ne '');
230 + for (@bag) {
231 + my ($a, $b) = @{$_};
232 + print "\n";
233 + compare_topics($a, $b);
234 + }
235 + }
236 +}
237 +
238 +sub compare_cooking {
239 + my ($old, $new) = @_;
240 +
241 + print compare_them([$old->{HEADER}], [$new->{HEADER}]);
242 + print compare_them([$old->{GREETING}], [$new->{GREETING}]);
243 +
244 + my (@sections, %sections, @topics, %topics, @fromto, %fromto);
245 +
246 + for my $section_name (@{$old->{SECTIONS}}, @{$new->{SECTIONS}}) {
247 + next if (exists $sections{$section_name});
248 + $sections{$section_name} = scalar @sections;
249 + push @sections, $section_name;
250 + }
251 +
252 + my $gone_class = "Gone topics";
253 + my $born_class = "Born topics";
254 + my $stay_class = "Other topics";
255 +
256 + push @fromto, $born_class;
257 + for my $topic_name (@{$old->{TOPIC_ORDER}}, @{$new->{TOPIC_ORDER}}) {
258 + next if (exists $topics{$topic_name});
259 + push @topics, $topic_name;
260 +
261 + my $oldtopic = $old->{TOPICS}{$topic_name};
262 + my $newtopic = $new->{TOPICS}{$topic_name};
263 + $topics{$topic_name} = +{
264 + OLD => $oldtopic,
265 + NEW => $newtopic,
266 + };
267 + my $oldsec = $oldtopic->{IN_SECTION};
268 + my $newsec = $newtopic->{IN_SECTION};
269 + if (defined $oldsec && defined $newsec) {
270 + if ($oldsec ne $newsec) {
271 + my $fromto =
272 + "Moved from [$oldsec] to [$newsec]";
273 + if (!exists $fromto{$fromto}) {
274 + $fromto{$fromto} = [];
275 + push @fromto, $fromto;
276 + }
277 + push @{$fromto{$fromto}}, $topic_name;
278 + } else {
279 + push @{$fromto{$stay_class}}, $topic_name;
280 + }
281 + } elsif (defined $oldsec) {
282 + push @{$fromto{$gone_class}}, $topic_name;
283 + } else {
284 + push @{$fromto{$born_class}}, $topic_name;
285 + }
286 + }
287 + push @fromto, $stay_class;
288 + push @fromto, $gone_class;
289 +
290 + for my $fromto (@fromto) {
291 + compare_class($fromto, $fromto{$fromto}, \%topics);
292 + }
293 +}