Просмотр исходного кода

dpkg-mergechangelogs: new script doing 3-way merge of Debian changelogs

Raphaël Hertzog лет назад: 16
Родитель
Сommit
9b98847665

+ 3 - 0
debian/changelog

@@ -43,6 +43,9 @@ dpkg (1.15.6.2) UNRELEASED; urgency=low
     conffiles and renaming conffiles. Closes: #514316
   * Fix "dpkg-scansources -e", it was calling a non-existing function.
     Closes: #578162
+  * Add new script dpkg-mergechangelogs to do 3-way merges of Debian
+    changelogs. Add libalgorithm-merge-perl to Recommends for the
+    benefit of this script.
 
   [ Colin Watson ]
   * Modern tar files typically use NormalFile1 rather than NormalFile0 for

+ 1 - 1
debian/control

@@ -52,7 +52,7 @@ Priority: optional
 Architecture: all
 Depends: libdpkg-perl (= ${source:Version}), bzip2, xz-utils,
  patch, make, binutils, base-files (>= 5.0.0), ${misc:Depends}
-Recommends: gcc | c-compiler, build-essential, fakeroot, gnupg, gpgv
+Recommends: gcc | c-compiler, build-essential, fakeroot, gnupg, gpgv, libalgorithm-merge-perl
 Suggests: debian-keyring
 Conflicts: dpkg-cross (<< 2.0.0), devscripts (<< 2.10.26)
 Description: Debian package development tools

+ 2 - 0
debian/dpkg-dev.install

@@ -9,6 +9,7 @@ usr/bin/dpkg-distaddfile
 usr/bin/dpkg-genchanges
 usr/bin/dpkg-gencontrol
 usr/bin/dpkg-gensymbols
+usr/bin/dpkg-mergechangelogs
 usr/bin/dpkg-name
 usr/bin/dpkg-parsechangelog
 usr/bin/dpkg-scanpackages
@@ -35,6 +36,7 @@ usr/share/man/{*/*,*}/dpkg-distaddfile.1
 usr/share/man/{*/*,*}/dpkg-genchanges.1
 usr/share/man/{*/*,*}/dpkg-gencontrol.1
 usr/share/man/{*/*,*}/dpkg-gensymbols.1
+usr/share/man/{*/*,*}/dpkg-mergechangelogs.1
 usr/share/man/{*/*,*}/dpkg-name.1
 usr/share/man/{*/*,*}/dpkg-parsechangelog.1
 usr/share/man/{*/*,*}/dpkg-scanpackages.1

+ 1 - 0
man/Makefile.am

@@ -106,6 +106,7 @@ dist_man_MANS = \
 	dpkg-genchanges.1 \
 	dpkg-gencontrol.1 \
 	dpkg-gensymbols.1 \
+	dpkg-mergechangelogs.1 \
 	dpkg-name.1 \
 	dpkg-parsechangelog.1 \
 	dpkg-query.1 \

+ 71 - 0
man/dpkg-mergechangelogs.1

@@ -0,0 +1,71 @@
+.TH dpkg\-mergechangelogs 1 "2010-04-18" "Debian Project" "dpkg suite"
+.SH NAME
+dpkg\-mergechangelogs \- 3-way merge of debian/changelog files
+.
+.SH SYNOPSIS
+.B dpkg\-mergechangelogs
+.RI [ option "...] " old " " new\-a " " new\-b " [" out ]
+.
+.SH DESCRIPTION
+.P
+This program will use the 3 provided versions of the Debian changelog to
+generate a merged changelog file. The resulting changelog is stored
+in the file \fIout\fP or output to the standard output if that parameter is
+not given.
+.P
+Each entry is identified by its version number and they are assumed to
+be not conflicting, they are simply merged in the right order (by
+decreasing version number). When \fB\-\-merge\-prereleases\fP is used, the
+part of the version number after the last tilde is dropped so that
+1.0-1~exp1 and 1.0-1~exp5 are considered to be the same entry. When the
+same version is available in both \fInew\-a\fP and \fInew\-b\fP, a standard
+line-based 3-way merge is attempted (provided that the module
+Algorithm::Merge is available \[em] it's part of the package
+libalgorithm-merge-perl \[em] otherwise you get a global conflict on the
+content of the entry).
+.
+.SH OPTIONS
+.TP
+.BR \-\-merge-prereleases ", " \-m
+Drop the part after the last tilde in the version number when doing
+version comparison to identify if two entries are supposed to be
+the same or not.
+.sp
+This is useful when you keep using the same changelog entry but you
+increase its version number regularly. For instance, you might have
+2.3-1~exp1, 2.3-1~exp2, ... until the official release 2.3-1 and they
+are all the same changelog entry that has evolved over time.
+.TP
+.B \-\-help
+Show the usage message and exit.
+.TP
+.B \-\-version
+Show the version and exit.
+.
+.SH LIMITATIONS
+.P
+Anything that is not parsed by Dpkg::Changelog is lost during the merge.
+This might include stuff like vim modelines, comments which were not
+supposed to be there, etc.
+.
+.SH INTEGRATION WITH GIT
+.P
+If you want to use this program to merge Debian changelog files in a git
+repository, you have first to register a new merge driver in
+\fB.git/config\fP or \fB~/.gitconfig\fP:
+.P
+ [merge "dpkg\-mergechangelogs"]
+     name = debian/changelog merge driver
+     driver = dpkg\-mergechangelogs \-m %O %A %B %A
+.P
+Then you have to setup the merge attribute for the debian/changelog file
+either in \fB.gitattributes\fP in the repository itself, or in
+\fB.git/info/attributes\fP:
+.P
+ debian/changelog merge=dpkg\-mergechangelogs
+.SH AUTHOR
+Copyright \(co 2009-2010 Rapha\[:e]l Hertzog
+.sp
+This is free software; see the GNU General Public Licence version 2 or
+later for copying conditions. There is NO WARRANTY.
+

+ 5 - 0
man/po/po4a.cfg

@@ -128,6 +128,11 @@
            add_$lang:po/$lang.add
 
 
+[type:man] dpkg-mergechangelogs.1     \
+           $lang:$(builddir)/$lang/dpkg-mergechangelogs.1   \
+           add_$lang:po/$lang.add
+
+
 [type:man] dpkg-name.1                \
            $lang:$(builddir)/$lang/dpkg-name.1              \
            add_$lang:po/$lang.add

+ 1 - 0
scripts/.gitignore

@@ -7,6 +7,7 @@ dpkg-divert
 dpkg-genchanges
 dpkg-gencontrol
 dpkg-gensymbols
+dpkg-mergechangelogs
 dpkg-name
 dpkg-parsechangelog
 dpkg-scanpackages

+ 2 - 0
scripts/Makefile.am

@@ -12,6 +12,7 @@ bin_SCRIPTS = \
 	dpkg-genchanges \
 	dpkg-gencontrol \
 	dpkg-gensymbols \
+	dpkg-mergechangelogs \
 	dpkg-name \
 	dpkg-parsechangelog \
 	dpkg-scanpackages \
@@ -40,6 +41,7 @@ EXTRA_DIST = \
 	dpkg-genchanges.pl \
 	dpkg-gencontrol.pl \
 	dpkg-gensymbols.pl \
+	dpkg-mergechangelogs.pl \
 	dpkg-name.pl \
 	dpkg-parsechangelog.pl \
 	dpkg-scanpackages.pl \

+ 289 - 0
scripts/dpkg-mergechangelogs.pl

@@ -0,0 +1,289 @@
+#!/usr/bin/perl
+
+# Copyright © 2009-2010 Raphaël Hertzog <hertzog@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 <http://www.gnu.org/licenses/>.
+
+use warnings;
+use strict;
+
+use Dpkg;
+use Dpkg::Changelog::Debian;
+use Dpkg::ErrorHandling;
+use Dpkg::Gettext;
+use Dpkg::Version;
+
+use Getopt::Long;
+use Scalar::Util qw(blessed);
+
+sub merge_entries($$$);
+sub merge_block($$$;&);
+sub merge_entry_item($$$$);
+sub merge_conflict($$);
+sub get_conflict_block($$);
+sub join_lines($);
+
+BEGIN {
+    eval 'use Algorithm::Merge qw(merge);';
+    if ($@) {
+	eval q{
+	    sub merge {
+		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"), $progname, $version;
+
+    printf "\n" . _g("Copyright (C) 2009-2010 Raphaël Hertzog.");
+
+    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:
+  --merge-prereleases, -m  merge pre-releases together, ignores everything
+                           after the last '~' in the version.
+  --help, -h, -?           show this help message.
+  --version                show the version.
+"), $progname);
+}
+
+my $merge_prereleases;
+unless (GetOptions('help|?' => sub { usage(); exit(0) },
+		   'merge-prereleases|m' => \$merge_prereleases,
+		   'version' => sub { version(); exit(0) })) {
+    usage();
+    exit(2);
+}
+my ($old, $new_a, $new_b, $out_file) = @ARGV;
+unless (defined $old and defined $new_a and defined $new_b and
+        -e $old and -e $new_a and -e $new_b)
+{
+    usage();
+    exit(2);
+}
+
+my ($cho, $cha, $chb, $res);
+$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 $tail = $_[0]->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(OUT, ">", $out_file) || syserr(_g("cannot write %s"), $out_file);
+    print OUT ((blessed $_) ? "$_" : "$_\n") foreach @result;
+    close(OUT);
+} 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 $minver;
+    foreach my $i (0 .. 2) {
+	if (defined $minver and defined $items[$i]) {
+	    my $cmp = compare_versions($minver, $items[$i]->get_version());
+	    if ($cmp > 0) {
+		$minver = $items[$i]->get_version();
+		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 {
+	    $minver = $items[$i]->get_version() if defined $items[$i];
+	}
+    }
+    return @items;
+}
+
+# Compares the versions taking into account some oddities like the fact
+# that we want backport/volatile 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;
+    $a = $a->get_version() if ref($a) and $a->isa("Dpkg::Changelog::Entry");
+    $b = $b->get_version() if ref($b) and $b->isa("Dpkg::Changelog::Entry");
+    # Backport and volatile are not real prereleases
+    $a =~ s/~(bpo|vola)/+$1/;
+    $b =~ s/~(bpo|vola)/+$1/;
+    if ($merge_prereleases) {
+	$a =~ s/~[^~]*$//;
+	$b =~ s/~[^~]*$//;
+    }
+    return version_compare($a, $b);
+}
+
+# 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 {
+				$exitcode = 1;
+				return get_conflict_block($_[0], $_[1]);
+			   }
+		       });
+    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 unless defined $preprocess;
+    $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, ">>>>>>");
+}
+

+ 1 - 0
scripts/po/POTFILES.in

@@ -8,6 +8,7 @@ scripts/dpkg-distaddfile.pl
 scripts/dpkg-genchanges.pl
 scripts/dpkg-gencontrol.pl
 scripts/dpkg-gensymbols.pl
+scripts/dpkg-mergechangelogs.pl
 scripts/dpkg-name.pl
 scripts/dpkg-parsechangelog.pl
 scripts/dpkg-scanpackages.pl