Blob Blame History Raw
#!/usr/bin/env perl
#############################################################################
# Copyright (c) 2008,2009 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# 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, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#############################################################################
#
# script to merge changes done to *.changes files
#
# To install the script as a git merge driver:
#
#   git config merge.rpm-changes.name "*.changes merge driver"
#   git config merge.rpm-changes.driver "scripts/rpm-changes-merge.pl %A %O %B"
#   echo '*.changes merge=rpm-changes' >>.git/info/attributes

use strict;
use warnings;

BEGIN {
    if ($0 =~ /^(.*)\/[^\/]*/) {
        unshift @INC, "$1/lib";
    } else {
        unshift @INC,  "./lib";
    }
}
use Time::Zone;
use Getopt::Std;
use POSIX qw(mktime);
use File::Temp qw(tempfile);

my $conflicts = 0;

sub usage {
    print STDERR
"Usage:
  Three-way merge:
    $0 [-p] my.changes orig.changes their.changes
  Two-way merge:
    $0 [-p] -2 my.changes their.changes
  Fixup mode:
    $0 [-p] -1 my.changes
";
    exit 1;
}

sub main {
    $ENV{'TZ'} = "UTC";

    our ($opt_p, $opt_1, $opt_2, $opt_3) = (0, 0, 0, 0);
    my (%O, %A, %B, $out);
    if (!getopts('p123') || $opt_1 + $opt_2 + $opt_3 > 1) {
        usage();
    }
    if ($opt_1) {
        usage() if @ARGV != 1;
        loadchanges($ARGV[0], \%A);
    } elsif ($opt_2) {
        usage() if @ARGV != 2;
        loadchanges($ARGV[0], \%A);
        loadchanges($ARGV[1], \%B);
    } else {
        usage() if @ARGV != 3;
        loadchanges($ARGV[0], \%A);
        loadchanges($ARGV[1], \%O);
        loadchanges($ARGV[2], \%B);
    }
    if ($opt_p) {
        $out = \*STDOUT;
    } else {
        open($out, '>', $ARGV[0]) or die "$ARGV[0]: $!\n";
    }

    my %seen;
    for my $key (reverse(sort(keys(%A), keys(%O), keys(%B)))) {
        next if $seen{$key};
        $seen{$key} = 1;
        print $out merge($A{$key}, $O{$key}, $B{$key});
    }
    exit $conflicts;
}

sub merge {
    my ($a, $o, $b) = @_;
    $a = "" unless defined $a;
    $o = "" unless defined $o;
    $b = "" unless defined $b;
    return $a if $a eq $b;
    return $a if $o eq $b;
    return $b if $o eq $a;
    return rcs_merge($a, $o, $b);
}

my $have_rcs_merge = 1;
sub rcs_merge {
    my @texts = @_;
    my $res;
    if ($have_rcs_merge) {
        my @fh;
        my @fn;
        for my $i (0..2) {
            ($fh[$i], $fn[$i]) = tempfile();
            my $fh = $fh[$i];
            print $fh $texts[$i];
        }
        $res = `merge -p $fn[0] $fn[1] $fn[2]`;
        for my $i (0..2) {
            close($fh[$i]);
            unlink($fn[$i]);
        }
        if ($? == 0) {
            return $res;
        } elsif ($? >> 8 == 1) {
            $conflicts = 1;
            return $res;
        } else {
            print STDERR "merge(1) not found, using DumbMerge(TM) instead.\n";
            print STDERR "Install the rcs package for better merge results.\n";
            $have_rcs_merge = 0;
        }
    }
    return "<<<<<<< $ARGV[0]\n" . $texts[0] . "=======\n" . $texts[2] . ">>>>>>> $ARGV[2]\n";
}

sub loadchanges {
    my ($file, $res) = @_;
    open(my $fh, '<', $file) or die "$file: $!\n";
    my $l;
    my $date = 1<<32;
    my $email = "";
    my $expect_date = 0;
    my $text;
    while ($l = <$fh>) {
        if ($expect_date) {
            (my $dt, $email) = parse_date($l);
            if (defined $dt) {
                $date = $dt;
            } else {
                print STDERR "$file:$.: invalid date: $l";
            }
            $expect_date = 0;
        }
        if ($l =~ /^-{50}-*$/ || eof($fh)) {
            if (eof($fh)) {
                $text .= $l;
            }
            if (defined $text) {
                my $key = sprintf("%010d %s", $date, $email);
                if (defined $res->{$key}) {
                    $res->{$key} .= $text;
                } else {
                    $res->{$key} = $text
                }
            }
            undef $text;
            $date--;
            $expect_date = 1;
        }
        $text .= $l;
    }
    close($fh)
}

my %monthnum = (
    jan => 0,
    feb => 1,
    mar => 2,
    apr => 3,
    may => 4,
    jun => 5,
    jul => 6,
    aug => 7,
    sep => 8,
    oct => 9,
    nov => 10,
    dec => 11,
);

sub parse_date {
    my $l = shift;
    if ($l !~ /^(?:mon|tue|wed|thu|fri|sat|sun) +(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) +(\d+) +(\d\d):(\d\d):(\d\d) +([A-Z]+) +(\d\d\d\d) +- +([^ ]*) *$/i) {
        return (undef, "");
    }
    my ($b, $d, $H, $M, $S, $Z, $Y, $email) = ($1, $2, $3, $4, $5, $6, $7, $8);
    my $date = mktime($S, $M, $H, $d, $monthnum{lc $b}, $Y - 1900);
    return (undef, "") unless defined $date;
    my $offset = tz_offset($Z);
    return (undef, "") unless defined $offset;
    chomp $email;
    return ($date - $offset, $email);
}

main();

# vim: sw=4:et