summaryrefslogtreecommitdiffstats
path: root/scripts/dpkg-mergechangelogs.pl
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/dpkg-mergechangelogs.pl')
-rwxr-xr-xscripts/dpkg-mergechangelogs.pl321
1 files changed, 321 insertions, 0 deletions
diff --git a/scripts/dpkg-mergechangelogs.pl b/scripts/dpkg-mergechangelogs.pl
new file mode 100755
index 0000000..68d6d79
--- /dev/null
+++ b/scripts/dpkg-mergechangelogs.pl
@@ -0,0 +1,321 @@
+#!/usr/bin/perl
+
+# Copyright © 2009-2010 Raphaël Hertzog <hertzog@debian.org>
+# Copyright © 2012 Guillem Jover <guillem@debian.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+use warnings;
+use strict;
+
+use Scalar::Util qw(blessed);
+use Getopt::Long qw(:config posix_default bundling_values no_ignorecase);
+
+use Dpkg ();
+use Dpkg::Changelog::Debian;
+use Dpkg::ErrorHandling;
+use Dpkg::Gettext;
+use Dpkg::Version;
+use Dpkg::Vendor qw(run_vendor_hook);
+
+textdomain('dpkg-dev');
+
+sub merge_entries($$$);
+sub merge_block($$$;&);
+sub merge_entry_item($$$$);
+sub merge_conflict($$);
+sub get_conflict_block($$);
+sub join_lines($);
+
+BEGIN {
+ eval q{
+ pop @INC if $INC[-1] eq '.';
+ use Algorithm::Merge qw(merge);
+ };
+ if ($@) {
+ *merge = sub {
+ my ($o, $a, $b) = @_;
+ return @$a if join("\n", @$a) eq join("\n", @$b);
+ return get_conflict_block($a, $b);
+ };
+ }
+}
+
+sub version {
+ printf g_("Debian %s version %s.\n"), $Dpkg::PROGNAME, $Dpkg::PROGVERSION;
+
+ printf "\n" . g_(
+'This is free software; see the GNU General Public License version 2 or
+later for copying conditions. There is NO warranty.
+');
+}
+
+sub usage {
+ printf g_(
+"Usage: %s [<option>...] <old> <new-a> <new-b> [<out>]
+
+Options:
+ -m, --merge-prereleases merge pre-releases together, ignores everything
+ after the last '~' in the version.
+ --merge-unreleased merge UNRELEASED entries together, ignoring their
+ version numbers.
+ -?, --help show this help message.
+ --version show the version.
+"), $Dpkg::PROGNAME;
+}
+
+my $merge_prereleases;
+my $merge_unreleased;
+
+my @options_spec = (
+ 'help|?' => sub { usage(); exit(0) },
+ 'version' => sub { version(); exit(0) },
+ 'merge-prereleases|m' => \$merge_prereleases,
+ 'merge-unreleased' => \$merge_unreleased,
+);
+
+{
+ local $SIG{__WARN__} = sub { usageerr($_[0]) };
+ GetOptions(@options_spec);
+}
+
+my $backport_version_regex = run_vendor_hook('backport-version-regex');
+
+my ($old, $new_a, $new_b, $out_file) = @ARGV;
+unless (defined $old and defined $new_a and defined $new_b)
+{
+ usageerr(g_('needs at least three arguments'));
+}
+unless (-e $old and -e $new_a and -e $new_b)
+{
+ usageerr(g_('file arguments need to exist'));
+}
+
+my ($cho, $cha, $chb);
+$cho = Dpkg::Changelog::Debian->new();
+$cho->load($old);
+$cha = Dpkg::Changelog::Debian->new();
+$cha->load($new_a);
+$chb = Dpkg::Changelog::Debian->new();
+$chb->load($new_b);
+
+my @o = reverse @$cho;
+my @a = reverse @$cha;
+my @b = reverse @$chb;
+
+my @result; # Lines to output
+my $exitcode = 0; # 1 if conflict encountered
+
+unless (merge_block($cho, $cha, $chb, sub {
+ my $changes = shift;
+ my $tail = $changes->get_unparsed_tail();
+ chomp $tail if defined $tail;
+ return $tail;
+ }))
+{
+ merge_conflict($cha->get_unparsed_tail(), $chb->get_unparsed_tail());
+}
+
+while (1) {
+ my ($o, $a, $b) = get_items_to_merge();
+ last unless defined $o or defined $a or defined $b;
+ next if merge_block($o, $a, $b);
+ # We only have the usually conflicting cases left
+ if (defined $a and defined $b) {
+ # Same entry, merge sub-items separately for a nicer result
+ merge_entries($o, $a, $b);
+ } else {
+ # Non-existing on one side, changed on the other side
+ merge_conflict($a, $b);
+ }
+}
+
+if (defined($out_file) and $out_file ne '-') {
+ open(my $out_fh, '>', $out_file)
+ or syserr(g_('cannot write %s'), $out_file);
+ print { $out_fh } ((blessed $_) ? "$_" : "$_\n") foreach @result;
+ close($out_fh) or syserr(g_('cannot write %s'), $out_file);
+} else {
+ print ((blessed $_) ? "$_" : "$_\n") foreach @result;
+}
+
+exit $exitcode;
+
+# Returns the next items to merge, all items returned correspond to the
+# same minimal version among the 3 possible next items (undef is returned
+# if the next item on the given changelog is skipped)
+sub get_items_to_merge {
+ my @items = (shift @o, shift @a, shift @b);
+ my @arrays = (\@o, \@a, \@b);
+ my $minitem;
+ foreach my $i (0 .. 2) {
+ if (defined $minitem and defined $items[$i]) {
+ my $cmp = compare_versions($minitem, $items[$i]);
+ if ($cmp > 0) {
+ $minitem = $items[$i];
+ foreach my $j (0 .. $i - 1) {
+ unshift @{$arrays[$j]}, $items[$j];
+ $items[$j] = undef;
+ }
+ } elsif ($cmp < 0) {
+ unshift @{$arrays[$i]}, $items[$i];
+ $items[$i] = undef;
+ }
+ } else {
+ $minitem = $items[$i] if defined $items[$i];
+ }
+ }
+ return @items;
+}
+
+# Compares the versions taking into account some oddities like the fact
+# that we want backport versions to sort higher than the version
+# on which they are based.
+sub compare_versions {
+ my ($a, $b) = @_;
+
+ return 0 if not defined $a and not defined $b;
+ return 1 if not defined $b;
+ return -1 if not defined $a;
+
+ my ($av, $bv) = ($a, $b);
+
+ $av = $a->get_version() if ref $a and $a->isa('Dpkg::Changelog::Entry');
+ $bv = $b->get_version() if ref $b and $b->isa('Dpkg::Changelog::Entry');
+
+ if ($merge_unreleased) {
+ return 0 if $a->get_distributions() eq 'UNRELEASED' and
+ $b->get_distributions() eq 'UNRELEASED';
+ }
+ # Backports are not real prereleases.
+ if (defined $backport_version_regex) {
+ $a =~ s/$backport_version_regex/+$1/;
+ $b =~ s/$backport_version_regex/+$1/;
+ }
+ if ($merge_prereleases) {
+ $av =~ s/~[^~]*$//;
+ $bv =~ s/~[^~]*$//;
+ }
+ $av = Dpkg::Version->new($av);
+ $bv = Dpkg::Version->new($bv);
+ return $av <=> $bv;
+}
+
+# Merge changelog entries smartly by merging individually the different
+# parts constituting an entry
+sub merge_entries($$$) {
+ my ($o, $a, $b) = @_;
+ # NOTE: Only $o can be undef
+
+ # Merge the trailer line
+ unless (merge_entry_item('blank_after_trailer', $o, $a, $b)) {
+ unshift @result, '';
+ }
+ unless (merge_entry_item('trailer', $o, $a, $b)) {
+ merge_conflict($a->get_part('trailer'), $b->get_part('trailer'));
+ }
+
+ # Merge the changes
+ unless (merge_entry_item('blank_after_changes', $o, $a, $b)) {
+ unshift @result, '';
+ }
+ my @merged = merge(defined $o ? $o->get_part('changes') : [],
+ $a->get_part('changes'), $b->get_part('changes'),
+ {
+ CONFLICT => sub {
+ my ($ca, $cb) = @_;
+ $exitcode = 1;
+ return get_conflict_block($ca, $cb);
+ }
+ });
+ unshift @result, @merged;
+
+ # Merge the header line
+ unless (merge_entry_item('blank_after_header', $o, $a, $b)) {
+ unshift @result, '';
+ }
+ unless (merge_entry_item('header', $o, $a, $b)) {
+ merge_conflict($a->get_part('header'), $b->get_part('header'));
+ }
+}
+
+sub join_lines($) {
+ my $array = shift;
+ return join("\n", @$array) if ref($array) eq 'ARRAY';
+ return $array;
+}
+
+# Try to merge the obvious cases, return 1 on success and 0 on failure
+# O A B
+# - x x => x
+# o o b => b
+# - - b => b
+# o a o => a
+# - a - => a
+sub merge_block($$$;&) {
+ my ($o, $a, $b, $preprocess) = @_;
+ $preprocess //= \&join_lines;
+ $o = $preprocess->($o) if defined $o;
+ $a = $preprocess->($a) if defined $a;
+ $b = $preprocess->($b) if defined $b;
+ return 1 if not defined($a) and not defined($b);
+ if (defined($a) and defined($b) and ($a eq $b)) {
+ unshift @result, $a;
+ } elsif ((defined($a) and defined($o) and ($a eq $o)) or
+ (not defined($a) and not defined($o))) {
+ unshift @result, $b if defined $b;
+ } elsif ((defined($b) and defined($o) and ($b eq $o)) or
+ (not defined($b) and not defined($o))) {
+ unshift @result, $a if defined $a;
+ } else {
+ return 0;
+ }
+ return 1;
+}
+
+sub merge_entry_item($$$$) {
+ my ($item, $o, $a, $b) = @_;
+ if (blessed($o) and $o->isa('Dpkg::Changelog::Entry')) {
+ $o = $o->get_part($item);
+ } elsif (ref $o) {
+ $o = $o->{$item};
+ }
+ if (blessed($a) and $a->isa('Dpkg::Changelog::Entry')) {
+ $a = $a->get_part($item);
+ } elsif (ref $a) {
+ $a = $a->{$item};
+ }
+ if (blessed($b) and $b->isa('Dpkg::Changelog::Entry')) {
+ $b = $b->get_part($item);
+ } elsif (ref $b) {
+ $b = $b->{$item};
+ }
+ return merge_block($o, $a, $b);
+}
+
+sub merge_conflict($$) {
+ my ($a, $b) = @_;
+ unshift @result, get_conflict_block($a, $b);
+ $exitcode = 1;
+}
+
+sub get_conflict_block($$) {
+ my ($a, $b) = @_;
+ my (@a, @b);
+ push @a, $a if defined $a;
+ push @b, $b if defined $b;
+ @a = @{$a} if ref($a) eq 'ARRAY';
+ @b = @{$b} if ref($b) eq 'ARRAY';
+ return ('<<<<<<<', @a, '=======', @b, '>>>>>>>');
+}