File: | dpkg-mergechangelogs.pl |
Coverage: | 77.5% |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | #!/usr/bin/perl | ||||||
2 | |||||||
3 | # Copyright © 2009-2010 Raphaël Hertzog <hertzog@debian.org> | ||||||
4 | # Copyright © 2012 Guillem Jover <guillem@debian.org> | ||||||
5 | # | ||||||
6 | # This program is free software; you can redistribute it and/or modify | ||||||
7 | # it under the terms of the GNU General Public License as published by | ||||||
8 | # the Free Software Foundation; either version 2 of the License, or | ||||||
9 | # (at your option) any later version. | ||||||
10 | # | ||||||
11 | # This program is distributed in the hope that it will be useful, | ||||||
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
14 | # GNU General Public License for more details. | ||||||
15 | # | ||||||
16 | # You should have received a copy of the GNU General Public License | ||||||
17 | # along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||||
18 | |||||||
19 | 4 4 4 | 6407 1 86 | use warnings; | ||||
20 | 4 4 4 | 8 4 44 | use strict; | ||||
21 | |||||||
22 | 4 4 4 | 5 3 173 | use Scalar::Util qw(blessed); | ||||
23 | 4 4 4 | 733 21109 9 | use Getopt::Long qw(:config posix_default bundling_values no_ignorecase); | ||||
24 | |||||||
25 | 4 4 4 | 785 4 40 | use Dpkg (); | ||||
26 | 4 4 4 | 493 4 67 | use Dpkg::Changelog::Debian; | ||||
27 | 4 4 4 | 8 3 108 | use Dpkg::ErrorHandling; | ||||
28 | 4 4 4 | 8 2 69 | use Dpkg::Gettext; | ||||
29 | 4 4 4 | 6 2 87 | use Dpkg::Version; | ||||
30 | 4 4 4 | 6 2 290 | use Dpkg::Vendor qw(run_vendor_hook); | ||||
31 | |||||||
32 | 4 | 134000 | textdomain('dpkg-dev'); | ||||
33 | |||||||
34 | sub merge_entries($$$); | ||||||
35 | sub merge_block($$$;&); | ||||||
36 | sub merge_entry_item($$$$); | ||||||
37 | sub merge_conflict($$); | ||||||
38 | sub get_conflict_block($$); | ||||||
39 | sub join_lines($); | ||||||
40 | |||||||
41 | BEGIN { | ||||||
42 | 4 4 4 4 | 110 560 25439 72 | eval q{ | ||||
43 | pop @INC if $INC[-1] eq '.'; | ||||||
44 | use Algorithm::Merge qw(merge); | ||||||
45 | }; | ||||||
46 | 4 | 5203 | if ($@) { | ||||
47 | *merge = sub { | ||||||
48 | 0 | 0 | my ($o, $a, $b) = @_; | ||||
49 | 0 | 0 | return @$a if join("\n", @$a) eq join("\n", @$b); | ||||
50 | 0 | 0 | return get_conflict_block($a, $b); | ||||
51 | 0 | 0 | }; | ||||
52 | } | ||||||
53 | } | ||||||
54 | |||||||
55 | sub version { | ||||||
56 | 0 | 0 | printf g_("Debian %s version %s.\n"), $Dpkg::PROGNAME, $Dpkg::PROGVERSION; | ||||
57 | |||||||
58 | 0 | 0 | printf "\n" . g_( | ||||
59 | 'This is free software; see the GNU General Public License version 2 or | ||||||
60 | later for copying conditions. There is NO warranty. | ||||||
61 | '); | ||||||
62 | } | ||||||
63 | |||||||
64 | sub usage { | ||||||
65 | 0 | 0 | printf g_( | ||||
66 | "Usage: %s [<option>...] <old> <new-a> <new-b> [<out>] | ||||||
67 | |||||||
68 | Options: | ||||||
69 | -m, --merge-prereleases merge pre-releases together, ignores everything | ||||||
70 | after the last '~' in the version. | ||||||
71 | --merge-unreleased merge UNRELEASED entries together, ignoring their | ||||||
72 | version numbers. | ||||||
73 | -?, --help show this help message. | ||||||
74 | --version show the version. | ||||||
75 | "), $Dpkg::PROGNAME; | ||||||
76 | } | ||||||
77 | |||||||
78 | 4 | 4 | my $merge_prereleases; | ||||
79 | my $merge_unreleased; | ||||||
80 | |||||||
81 | my @options_spec = ( | ||||||
82 | 0 0 | 0 0 | 'help|?' => sub { usage(); exit(0) }, | ||||
83 | 0 0 | 0 0 | 'version' => sub { version(); exit(0) }, | ||||
84 | 4 | 14 | 'merge-prereleases|m' => \$merge_prereleases, | ||||
85 | 'merge-unreleased' => \$merge_unreleased, | ||||||
86 | ); | ||||||
87 | |||||||
88 | { | ||||||
89 | 4 4 0 | 4 12 0 | local $SIG{__WARN__} = sub { usageerr($_[0]) }; | ||||
90 | 4 | 7 | GetOptions(@options_spec); | ||||
91 | } | ||||||
92 | |||||||
93 | 4 | 690 | my $backport_version_regex = run_vendor_hook('backport-version-regex'); | ||||
94 | |||||||
95 | 4 | 8 | my ($old, $new_a, $new_b, $out_file) = @ARGV; | ||||
96 | 4 | 17 | unless (defined $old and defined $new_a and defined $new_b) | ||||
97 | { | ||||||
98 | 0 | 0 | usageerr(g_('needs at least three arguments')); | ||||
99 | } | ||||||
100 | 4 | 41 | unless (-e $old and -e $new_a and -e $new_b) | ||||
101 | { | ||||||
102 | 0 | 0 | usageerr(g_('file arguments need to exist')); | ||||
103 | } | ||||||
104 | |||||||
105 | 4 | 4 | my ($cho, $cha, $chb); | ||||
106 | 4 | 18 | $cho = Dpkg::Changelog::Debian->new(); | ||||
107 | 4 | 10 | $cho->load($old); | ||||
108 | 4 | 20 | $cha = Dpkg::Changelog::Debian->new(); | ||||
109 | 4 | 6 | $cha->load($new_a); | ||||
110 | 4 | 13 | $chb = Dpkg::Changelog::Debian->new(); | ||||
111 | 4 | 6 | $chb->load($new_b); | ||||
112 | |||||||
113 | 4 | 12 | my @o = reverse @$cho; | ||||
114 | 4 | 4 | my @a = reverse @$cha; | ||||
115 | 4 | 4 | my @b = reverse @$chb; | ||||
116 | |||||||
117 | 4 | 3 | my @result; # Lines to output | ||||
118 | 4 | 3 | my $exitcode = 0; # 1 if conflict encountered | ||||
119 | |||||||
120 | 4 | 12 | unless (merge_block($cho, $cha, $chb, sub { | ||||
121 | 12 | 7 | my $changes = shift; | ||||
122 | 12 | 16 | my $tail = $changes->get_unparsed_tail(); | ||||
123 | 12 | 11 | chomp $tail if defined $tail; | ||||
124 | 12 | 7 | return $tail; | ||||
125 | })) | ||||||
126 | { | ||||||
127 | 0 | 0 | merge_conflict($cha->get_unparsed_tail(), $chb->get_unparsed_tail()); | ||||
128 | } | ||||||
129 | |||||||
130 | 4 | 8 | while (1) { | ||||
131 | 36 | 24 | my ($o, $a, $b) = get_items_to_merge(); | ||||
132 | 36 | 55 | last unless defined $o or defined $a or defined $b; | ||||
133 | 32 | 22 | next if merge_block($o, $a, $b); | ||||
134 | # We only have the usually conflicting cases left | ||||||
135 | 10 | 18 | if (defined $a and defined $b) { | ||||
136 | # Same entry, merge sub-items separately for a nicer result | ||||||
137 | 10 | 8 | merge_entries($o, $a, $b); | ||||
138 | } else { | ||||||
139 | # Non-existing on one side, changed on the other side | ||||||
140 | 0 | 0 | merge_conflict($a, $b); | ||||
141 | } | ||||||
142 | } | ||||||
143 | |||||||
144 | 4 | 6 | if (defined($out_file) and $out_file ne '-') { | ||||
145 | 0 | 0 | open(my $out_fh, '>', $out_file) | ||||
146 | or syserr(g_('cannot write %s'), $out_file); | ||||||
147 | 0 0 | 0 0 | print { $out_fh } ((blessed $_) ? "$_" : "$_\n") foreach @result; | ||||
148 | 0 | 0 | close($out_fh) or syserr(g_('cannot write %s'), $out_file); | ||||
149 | } else { | ||||||
150 | 4 | 28 | print ((blessed $_) ? "$_" : "$_\n") foreach @result; | ||||
151 | } | ||||||
152 | |||||||
153 | 4 | 33 | exit $exitcode; | ||||
154 | |||||||
155 | # Returns the next items to merge, all items returned correspond to the | ||||||
156 | # same minimal version among the 3 possible next items (undef is returned | ||||||
157 | # if the next item on the given changelog is skipped) | ||||||
158 | sub get_items_to_merge { | ||||||
159 | 36 | 26 | my @items = (shift @o, shift @a, shift @b); | ||||
160 | 36 | 23 | my @arrays = (\@o, \@a, \@b); | ||||
161 | 36 | 15 | my $minitem; | ||||
162 | 36 | 27 | foreach my $i (0 .. 2) { | ||||
163 | 108 | 110 | if (defined $minitem and defined $items[$i]) { | ||||
164 | 54 | 39 | my $cmp = compare_versions($minitem, $items[$i]); | ||||
165 | 54 | 63 | if ($cmp > 0) { | ||||
166 | 5 | 4 | $minitem = $items[$i]; | ||||
167 | 5 | 4 | foreach my $j (0 .. $i - 1) { | ||||
168 | 8 8 | 4 5 | unshift @{$arrays[$j]}, $items[$j]; | ||||
169 | 8 | 6 | $items[$j] = undef; | ||||
170 | } | ||||||
171 | } elsif ($cmp < 0) { | ||||||
172 | 12 12 | 4 11 | unshift @{$arrays[$i]}, $items[$i]; | ||||
173 | 12 | 9 | $items[$i] = undef; | ||||
174 | } | ||||||
175 | } else { | ||||||
176 | 54 | 48 | $minitem = $items[$i] if defined $items[$i]; | ||||
177 | } | ||||||
178 | } | ||||||
179 | 36 | 34 | return @items; | ||||
180 | } | ||||||
181 | |||||||
182 | # Compares the versions taking into account some oddities like the fact | ||||||
183 | # that we want backport versions to sort higher than the version | ||||||
184 | # on which they are based. | ||||||
185 | sub compare_versions { | ||||||
186 | 54 | 27 | my ($a, $b) = @_; | ||||
187 | |||||||
188 | 54 | 39 | return 0 if not defined $a and not defined $b; | ||||
189 | 54 | 38 | return 1 if not defined $b; | ||||
190 | 54 | 34 | return -1 if not defined $a; | ||||
191 | |||||||
192 | 54 | 29 | my ($av, $bv) = ($a, $b); | ||||
193 | |||||||
194 | 54 | 118 | $av = $a->get_version() if ref $a and $a->isa('Dpkg::Changelog::Entry'); | ||||
195 | 54 | 99 | $bv = $b->get_version() if ref $b and $b->isa('Dpkg::Changelog::Entry'); | ||||
196 | |||||||
197 | 54 | 42 | if ($merge_unreleased) { | ||||
198 | 4 | 3 | return 0 if $a->get_distributions() eq 'UNRELEASED' and | ||||
199 | $b->get_distributions() eq 'UNRELEASED'; | ||||||
200 | } | ||||||
201 | # Backports are not real prereleases. | ||||||
202 | 53 | 32 | if (defined $backport_version_regex) { | ||||
203 | 53 | 60 | $a =~ s/$backport_version_regex/+$1/; | ||||
204 | 53 | 59 | $b =~ s/$backport_version_regex/+$1/; | ||||
205 | } | ||||||
206 | 53 | 47 | if ($merge_prereleases) { | ||||
207 | 24 | 19 | $av =~ s/~[^~]*$//; | ||||
208 | 24 | 13 | $bv =~ s/~[^~]*$//; | ||||
209 | } | ||||||
210 | 53 | 56 | $av = Dpkg::Version->new($av); | ||||
211 | 53 | 41 | $bv = Dpkg::Version->new($bv); | ||||
212 | 53 | 42 | return $av <=> $bv; | ||||
213 | } | ||||||
214 | |||||||
215 | # Merge changelog entries smartly by merging individually the different | ||||||
216 | # parts constituting an entry | ||||||
217 | sub merge_entries($$$) { | ||||||
218 | 10 | 7 | my ($o, $a, $b) = @_; | ||||
219 | # NOTE: Only $o can be undef | ||||||
220 | |||||||
221 | # Merge the trailer line | ||||||
222 | 10 | 9 | unless (merge_entry_item('blank_after_trailer', $o, $a, $b)) { | ||||
223 | 0 | 0 | unshift @result, ''; | ||||
224 | } | ||||||
225 | 10 | 9 | unless (merge_entry_item('trailer', $o, $a, $b)) { | ||||
226 | 3 | 3 | merge_conflict($a->get_part('trailer'), $b->get_part('trailer')); | ||||
227 | } | ||||||
228 | |||||||
229 | # Merge the changes | ||||||
230 | 10 | 21 | unless (merge_entry_item('blank_after_changes', $o, $a, $b)) { | ||||
231 | 0 | 0 | unshift @result, ''; | ||||
232 | } | ||||||
233 | my @merged = merge(defined $o ? $o->get_part('changes') : [], | ||||||
234 | $a->get_part('changes'), $b->get_part('changes'), | ||||||
235 | { | ||||||
236 | CONFLICT => sub { | ||||||
237 | 3 | 986 | my ($ca, $cb) = @_; | ||||
238 | 3 | 1 | $exitcode = 1; | ||||
239 | 3 | 3 | return get_conflict_block($ca, $cb); | ||||
240 | } | ||||||
241 | 10 | 12 | }); | ||||
242 | 10 | 4921 | unshift @result, @merged; | ||||
243 | |||||||
244 | # Merge the header line | ||||||
245 | 10 | 6 | unless (merge_entry_item('blank_after_header', $o, $a, $b)) { | ||||
246 | 0 | 0 | unshift @result, ''; | ||||
247 | } | ||||||
248 | 10 | 8 | unless (merge_entry_item('header', $o, $a, $b)) { | ||||
249 | 4 | 6 | merge_conflict($a->get_part('header'), $b->get_part('header')); | ||||
250 | } | ||||||
251 | } | ||||||
252 | |||||||
253 | sub join_lines($) { | ||||||
254 | 209 | 97 | my $array = shift; | ||||
255 | 209 | 155 | return join("\n", @$array) if ref($array) eq 'ARRAY'; | ||||
256 | 125 | 58 | return $array; | ||||
257 | } | ||||||
258 | |||||||
259 | # Try to merge the obvious cases, return 1 on success and 0 on failure | ||||||
260 | # O A B | ||||||
261 | # - x x => x | ||||||
262 | # o o b => b | ||||||
263 | # - - b => b | ||||||
264 | # o a o => a | ||||||
265 | # - a - => a | ||||||
266 | sub merge_block($$$;&) { | ||||||
267 | 86 | 55 | my ($o, $a, $b, $preprocess) = @_; | ||||
268 | 86 | 132 | $preprocess //= \&join_lines; | ||||
269 | 86 | 57 | $o = $preprocess->($o) if defined $o; | ||||
270 | 86 | 55 | $a = $preprocess->($a) if defined $a; | ||||
271 | 86 | 64 | $b = $preprocess->($b) if defined $b; | ||||
272 | 86 | 70 | return 1 if not defined($a) and not defined($b); | ||||
273 | 82 | 245 | if (defined($a) and defined($b) and ($a eq $b)) { | ||||
274 | 48 | 33 | unshift @result, $a; | ||||
275 | } elsif ((defined($a) and defined($o) and ($a eq $o)) or | ||||||
276 | (not defined($a) and not defined($o))) { | ||||||
277 | 6 | 9 | unshift @result, $b if defined $b; | ||||
278 | } elsif ((defined($b) and defined($o) and ($b eq $o)) or | ||||||
279 | (not defined($b) and not defined($o))) { | ||||||
280 | 11 | 13 | unshift @result, $a if defined $a; | ||||
281 | } else { | ||||||
282 | 17 | 22 | return 0; | ||||
283 | } | ||||||
284 | 65 | 65 | return 1; | ||||
285 | } | ||||||
286 | |||||||
287 | sub merge_entry_item($$$$) { | ||||||
288 | 50 | 37 | my ($item, $o, $a, $b) = @_; | ||||
289 | 50 | 98 | if (blessed($o) and $o->isa('Dpkg::Changelog::Entry')) { | ||||
290 | 40 | 32 | $o = $o->get_part($item); | ||||
291 | } elsif (ref $o) { | ||||||
292 | 0 | 0 | $o = $o->{$item}; | ||||
293 | } | ||||||
294 | 50 | 95 | if (blessed($a) and $a->isa('Dpkg::Changelog::Entry')) { | ||||
295 | 50 | 33 | $a = $a->get_part($item); | ||||
296 | } elsif (ref $a) { | ||||||
297 | 0 | 0 | $a = $a->{$item}; | ||||
298 | } | ||||||
299 | 50 | 90 | if (blessed($b) and $b->isa('Dpkg::Changelog::Entry')) { | ||||
300 | 50 | 31 | $b = $b->get_part($item); | ||||
301 | } elsif (ref $b) { | ||||||
302 | 0 | 0 | $b = $b->{$item}; | ||||
303 | } | ||||||
304 | 50 | 37 | return merge_block($o, $a, $b); | ||||
305 | } | ||||||
306 | |||||||
307 | sub merge_conflict($$) { | ||||||
308 | 7 | 6 | my ($a, $b) = @_; | ||||
309 | 7 | 6 | unshift @result, get_conflict_block($a, $b); | ||||
310 | 7 | 6 | $exitcode = 1; | ||||
311 | } | ||||||
312 | |||||||
313 | sub get_conflict_block($$) { | ||||||
314 | 10 | 7 | my ($a, $b) = @_; | ||||
315 | 10 | 4 | my (@a, @b); | ||||
316 | 10 | 9 | push @a, $a if defined $a; | ||||
317 | 10 | 8 | push @b, $b if defined $b; | ||||
318 | 10 3 | 8 3 | @a = @{$a} if ref($a) eq 'ARRAY'; | ||||
319 | 10 3 | 9 1 | @b = @{$b} if ref($b) eq 'ARRAY'; | ||||
320 | 10 | 21 | return ('<<<<<<<', @a, '=======', @b, '>>>>>>>'); | ||||
321 | } |