#!/usr/bin/perl
my $VERSION = '1.0.0';
=head1 NAME
git export-debian-patches
=head1 SYNOPSIS
git export-debian-patches [OPTIONS]
=head1 DESCRIPTION
This program must be run from inside a Debian source package that is maintained
in a Git repository. It will generate a I directory with one
patch for each commit in the Debian packaging branch that do not touch files
under I.
Commits that are already applied upstream are not exported. Commits were not
applied upstream, but were reversed by later commits, are also not applied.
By default, the Debian packaging branch is assumed to be the I branch,
and the upstream branch is assumed to be I branch. You can change
these defaults with command-line options (see below).
=head1 ASSUMPTIONS
This tool makes a fair number of assumptions of how the package is being
maintained in a Git repository:
=over
=item
Debian packaging and upstream source are kept in separate branches.
=item
The upstream sources branch is merged into the Debian packaging branch.
=item
Changes to the upstream source made are merged into the Debian package branch.
=item
There are no commits that touch both files under I and files
outside it, i.e. all commits are either strictly Debian packaging changes B
upstream source changes.
=item
Last, but not least, each commits that changes upstream source and is merged
into the Debian packaging branch is self-contained and makes sense on its own.
If you have a series of commits that only make sense together, you will end up
with one patch for each commit.
=back
One important assumption that this tools I make is how exactly you
manage these patches. You can keep them on topic branches and merge everything
together with the Debian packaging in a build branch. You can keep commits
applied in the main Debian packaging branch. You can have a separate branch
with all patches for upstream and merge it into the Debian packaging branch.
As long as all the needed commits are merged into the I branch (or
whatever branch you pass to the I<--debian-branch> option), the relevant
commits will be exported as patches into I.
=head1 COMMAND LINE OPTIONS
=over
=item B<--debian-branch=>BRANCH
Uses I as Debian packaging branch. Default: "master".
=item B<--upstream-branch=>BRANCH
Uses I as upstream branch. Default: "upstream".
=back
=head1 MODE OF OPERATION
=cut
use strict;
use warnings;
use YAML;
use File::Basename;
use File::Temp qw/ tempdir /;
use File::Copy;
use Getopt::Long;
use Pod::Usage;
# Defaults
my $upstream_branch = 'upstream';
my $debian_branch = 'master';
GetOptions(
'd|debian-branch=s' => \$debian_branch,
'u|upstream-branch=s' => \$upstream_branch,
) or pod2usage(1);
scalar(@ARGV) == 0 or pod2usage(1); # no command line arguments accepted
sub run($) {
my ($command) = @_;
unless (system("$command") == 0) {
my $exitstatus = ($? >> 8);
exit($exitstatus);
}
}
# detect whether we are in a valid git repository. If we are not, nothing
# further happens.
run("git symbolic-ref HEAD > /dev/null");
my $range = "$upstream_branch..$debian_branch";
=pod
First, all commits present in the Debian packaging branch but not in the
upstrean branch are listed.
=cut
# List all commits in the specified range and transform the stream in a valid
# YAML stream. The stream contains one list per commit. The first element of the
# list is the commit SHA1, and the remaining elements are the files changed by
# that commit.
my $yaml_stream = '';
open(GIT, "git log $range --format=---%n%H --reverse --name-only|");
while (my $line = ) {
if ($line !~ /^\s*$/ ) { # skip blank lines
if ($line !~ /^---/) {
$line = "- $line";
}
$yaml_stream .= $line;
}
}
close(GIT);
# Load the YAML stream
my @commits = Load($yaml_stream);
exit(0) unless (@commits); # do not proceed with there are no commits to process
=pod
Then a list of upstream commits is created by picking the commits in the
original list that do not change any files under I.
=cut
my @upstream_patches = ();
for my $commit (@commits) {
my $sha1 = shift @$commit;
my $debian = grep { $_ =~ /^debian\//} @$commit;
if (!$debian) {
push @upstream_patches, $sha1;
print $sha1, "\n";
}
}
=pod
Then a local temporary branch is created off the upstream branch, and all
selected commits are cherry-pick'ed into that branch. The temporary branch is
then rebased against the upstream branch. This will drop all commits that were
already applied upstream.
=cut
my $tmp_branch = 'git-debian-patches-tmp';
run("git checkout -b $tmp_branch $upstream_branch");
for my $sha1 (@upstream_patches) {
run("git cherry-pick $sha1");
}
run("git rebase $upstream_branch");
=pod
After removing commits already applied upstream, the remaining patches are
exported to a temporary directory.
=cut
my $tmp_dir = tempdir(CLEANUP => 1);
run("git format-patch --output $tmp_dir $upstream_branch..$tmp_branch");
my @patches = glob("$tmp_dir/*");
=pod
In that directory we exclude any pair of patches in which the later reverses
the former. The fact that one patch reverses the other is detected using
B: if patches p1 and p2 result in an empty patch when combined,
then p2 reverts p1.
=cut
sub reversed($$) {
my ($p1, $p2) = @_;
open(DIFF, "combinediff $p1 $p2|");
my @difflines = ;
close(DIFF);
chomp @difflines;
my $diff = join('', @difflines);
if (!$diff) {
# diff is empty, $p2 reverses $p1
return 1;
} else {
return 0;
}
}
my %dropped = ();
my $n = scalar(@patches);
for (my $i = 0; $i < $n; $i++) {
my $p1 = $patches[$i];
for (my $j = $i+1; $j < $n; $j++) {
my $p2 = $patches[$j];
if (reversed($p1,$p2)) {
$dropped{$p1} = 1;
$dropped{$p2} = 1;
}
}
}
@patches = grep { !$dropped{$_} } @patches;
=pod
Finally the Debian packaging branch is checked out, the temporary branch is
removed, B are deleted>, the patches are copied
in I and a patch series file is written to
I.
=cut
run("git checkout $debian_branch");
run("git branch -D $tmp_branch");
run("mkdir -p debian/patches");
run("rm -rf debian/patches/*");
open(SERIES, '>', 'debian/patches/series');
for my $patch (map { basename($_) } @patches) {
copy("$tmp_dir/$patch", "debian/patches/");
print SERIES $patch, "\n";
}
close(SERIES);
=pod
=head1 SEE ALSO
B,
B,
B,
B,
B.
=head1 COPYRIGHT
Copyright (c) 2011, Antonio Terceiro
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 3 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 .
=cut