chiark / gitweb /
debian/control: Add missing dependency on List::MoreUtils
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 1a5b88a..5bd39e7 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -2,7 +2,7 @@
 # dgit
 # Integration between git and Debian-style archives
 #
-# Copyright (C)2013-2015 Ian Jackson
+# Copyright (C)2013-2016 Ian Jackson
 #
 # 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
@@ -600,36 +600,49 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit-distro.test-dummy.upload-host' => 'test-dummy',
                );
 
-our %gitcfg;
+our %gitcfgs;
+our @gitcfgsources = qw(cmdline local global system);
 
 sub git_slurp_config () {
     local ($debuglevel) = $debuglevel-2;
     local $/="\0";
 
-    my @cmd = (@git, qw(config -z --get-regexp .*));
-    debugcmd "|",@cmd;
-
-    open GITS, "-|", @cmd or die $!;
-    while (<GITS>) {
-       chomp or die;
-       printdebug "=> ", (messagequote $_), "\n";
-       m/\n/ or die "$_ ?";
-       push @{ $gitcfg{$`} }, $'; #';
+    # This algoritm is a bit subtle, but this is needed so that for
+    # options which we want to be single-valued, we allow the
+    # different config sources to override properly.  See #835858.
+    foreach my $src (@gitcfgsources) {
+       next if $src eq 'cmdline';
+       # we do this ourselves since git doesn't handle it
+       
+       my @cmd = (@git, qw(config -z --get-regexp), "--$src", qw(.*));
+       debugcmd "|",@cmd;
+
+       open GITS, "-|", @cmd or die $!;
+       while (<GITS>) {
+           chomp or die;
+           printdebug "=> ", (messagequote $_), "\n";
+           m/\n/ or die "$_ ?";
+           push @{ $gitcfgs{$src}{$`} }, $'; #';
+       }
+       $!=0; $?=0;
+       close GITS
+           or ($!==0 && $?==256)
+           or failedcmd @cmd;
     }
-    $!=0; $?=0;
-    close GITS
-       or ($!==0 && $?==256)
-       or failedcmd @cmd;
 }
 
 sub git_get_config ($) {
     my ($c) = @_;
-    my $l = $gitcfg{$c};
-    printdebug"C $c ".(defined $l ? messagequote "'$l'" : "undef")."\n"
-       if $debuglevel >= 4;
-    $l or return undef;
-    @$l==1 or badcfg "multiple values for $c" if @$l > 1;
-    return $l->[0];
+    foreach my $src (@gitcfgsources) {
+       my $l = $gitcfgs{$src}{$c};
+       printdebug"C $c ".(defined $l ? messagequote "'$l'" : "undef")."\n"
+           if $debuglevel >= 4;
+       $l or next;
+       @$l==1 or badcfg "multiple values for $c".
+           " (in $src git config)" if @$l > 1;
+       return $l->[0];
+    }
+    return undef;
 }
 
 sub cfg {
@@ -1414,10 +1427,13 @@ sub mktree_in_ud_from_only_subdir (;$) {
     return ($tree,$dir);
 }
 
+our @files_csum_info_fields = 
+    (['Checksums-Sha256','Digest::SHA', 'new(256)'],
+     ['Checksums-Sha1',  'Digest::SHA', 'new(1)'],
+     ['Files',           'Digest::MD5', 'new()']);
+
 sub dsc_files_info () {
-    foreach my $csumi (['Checksums-Sha256','Digest::SHA', 'new(256)'],
-                      ['Checksums-Sha1',  'Digest::SHA', 'new(1)'],
-                      ['Files',           'Digest::MD5', 'new()']) {
+    foreach my $csumi (@files_csum_info_fields) {
        my ($fname, $module, $method) = @$csumi;
        my $field = $dsc->{$fname};
        next unless defined $field;
@@ -1445,6 +1461,65 @@ sub dsc_files () {
     map { $_->{Filename} } dsc_files_info();
 }
 
+sub files_compare_inputs (@) {
+    my $inputs = \@_;
+    my %record;
+    my %fchecked;
+
+    my $showinputs = sub {
+       return join "; ", map { $_->get_option('name') } @$inputs;
+    };
+
+    foreach my $in (@$inputs) {
+       my $expected_files;
+       my $in_name = $in->get_option('name');
+
+       printdebug "files_compare_inputs $in_name\n";
+
+       foreach my $csumi (@files_csum_info_fields) {
+           my ($fname) = @$csumi;
+           printdebug "files_compare_inputs $in_name $fname\n";
+
+           my $field = $in->{$fname};
+           next unless defined $field;
+
+           my @files;
+           foreach (split /\n/, $field) {
+               next unless m/\S/;
+
+               my ($info, $f) = m/^(\w+ \d+) (?:\S+ \S+ )?(\S+)$/ or
+                   fail "could not parse $in_name $fname line \`$_'";
+
+               printdebug "files_compare_inputs $in_name $fname $f\n";
+
+               push @files, $f;
+
+               my $re = \ $record{$f}{$fname};
+               if (defined $$re) {
+                   $fchecked{$f}{$in_name} = 1;
+                   $$re eq $info or
+                       fail "hash or size of $f varies in $fname fields".
+                       " (between: ".$showinputs->().")";
+               } else {
+                   $$re = $info;
+               }
+           }
+           @files = sort @files;
+           $expected_files //= \@files;
+           "@$expected_files" eq "@files" or
+               fail "file list in $in_name varies between hash fields!";
+       }
+       $expected_files or
+           fail "$in_name has no files list field(s)";
+    }
+    printdebug "files_compare_inputs ".Dumper(\%fchecked, \%record)
+       if $debuglevel>=2;
+
+    grep { keys %$_ == @$inputs-1 } values %fchecked
+       or fail "no file appears in all file lists".
+       " (looked in: ".$showinputs->().")";
+}
+
 sub is_orig_file_in_dsc ($$) {
     my ($f, $dsc_files_info) = @_;
     return 0 if @$dsc_files_info <= 1;
@@ -2136,6 +2211,8 @@ sub mergeinfo_version ($) {
 }
 
 sub fetch_from_archive () {
+    ensure_setup_existing_tree();
+
     # Ensures that lrref() is what is actually in the archive, one way
     # or another, according to us - ie this client's
     # appropritaely-updated archive view.  Also returns the commit id.
@@ -2535,6 +2612,13 @@ sub setup_useremail (;$) {
     $setup->('name', 'DEBFULLNAME');
 }
 
+sub ensure_setup_existing_tree () {
+    my $k = "remote.$remotename.skipdefaultupdate";
+    my $c = git_get_config $k;
+    return if defined $c;
+    set_local_git_config $k, 'true';
+}
+
 sub setup_new_tree () {
     setup_mergechangelogs();
     setup_useremail();
@@ -2630,7 +2714,11 @@ sub commit_quilty_patch () {
     }
     my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds;
     runcmd_ordryrun_local @git, qw(add -f), @adds;
-    commit_admin "Commit Debian 3.0 (quilt) metadata";
+    commit_admin <<END
+Commit Debian 3.0 (quilt) metadata
+
+[dgit ($our_version) quilt-fixup]
+END
 }
 
 sub get_source_format () {
@@ -3123,17 +3211,20 @@ END
     my ($tree,$dir) = mktree_in_ud_from_only_subdir();
     check_for_vendor_patches() if madformat($dsc->{format});
     changedir '../../../..';
-    my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet';
-    my @diffcmd = (@git, qw(diff), $diffopt, $tree, $dgithead);
+    my @diffcmd = (@git, qw(diff --quiet), $tree, $dgithead);
     debugcmd "+",@diffcmd;
     $!=0; $?=-1;
     my $r = system @diffcmd;
     if ($r) {
        if ($r==256) {
-           fail "$dscfn specifies a different tree to your HEAD commit;".
-               " perhaps you forgot to build".
-               ($diffopt eq '--exit-code' ? "" :
-                " (run with -D to see full diff output)");
+           my $diffs = cmdoutput @git, qw(diff --stat), $tree, $dgithead;
+           fail <<END
+HEAD specifies a different tree to $dscfn:
+$diffs
+Perhaps you forgot to build.  Or perhaps there is a problem with your
+ source tree (see dgit(7) for some hints).  To see a full diff, run
+   git diff $tree HEAD
+END
        } else {
            failedcmd @diffcmd;
        }
@@ -3150,6 +3241,10 @@ END
        $changesfile = "$buildproductsdir/$changesfile";
     }
 
+    # Check that changes and .dsc agree enough
+    $changesfile =~ m{[^/]*$};
+    files_compare_inputs($dsc, parsecontrol($changesfile,$&));
+
     # Checks complete, we're going to try and go ahead:
 
     responder_send_file('changes',$changesfile);
@@ -3217,7 +3312,8 @@ END
        push @pushrefs, $forceflag."refs/tags/$tw->{Tag}";
     }
 
-    runcmd_ordryrun @git, qw(push),access_giturl(), @pushrefs;
+    runcmd_ordryrun @git,
+       qw(-c push.followTags=false push), access_giturl(), @pushrefs;
     runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), $dgithead;
 
     supplementary_message(<<'END');
@@ -3288,7 +3384,7 @@ sub cmd_clone {
            }
            if (stat $dstdir) {
                rmtree($dstdir) or die "remove $dstdir: $!\n";
-           } elsif (!grep { $! == $_ }
+           } elsif (grep { $! == $_ }
                     (ENOENT, ENOTDIR, EACCES, EPERM, ELOOP)) {
            } else {
                print STDERR "check whether to remove $dstdir: $!\n";
@@ -3637,13 +3733,10 @@ sub quiltify_dpkg_commit ($$$;$) {
     mkpath '.git/dgit';
     my $descfn = ".git/dgit/quilt-description.tmp";
     open O, '>', $descfn or die "$descfn: $!";
-    $msg =~ s/\s+$//g;
-    $msg =~ s/\n/\n /g;
-    $msg =~ s/^\s+$/ ./mg;
+    $msg =~ s/\n+/\n\n/;
     print O <<END or die $!;
-Description: $msg
-Author: $author
-$xinfo
+From: $author
+${xinfo}Subject: $msg
 ---
 
 END
@@ -3657,22 +3750,51 @@ END
     }
 }
 
-sub quiltify_trees_differ ($$;$$) {
-    my ($x,$y,$finegrained,$ignorenamesr) = @_;
+sub quiltify_trees_differ ($$;$$$) {
+    my ($x,$y,$finegrained,$ignorenamesr,$unrepres) = @_;
     # returns true iff the two tree objects differ other than in debian/
     # with $finegrained,
     # returns bitmask 01 - differ in upstream files except .gitignore
     #                 02 - differ in .gitignore
     # if $ignorenamesr is defined, $ingorenamesr->{$fn}
     #  is set for each modified .gitignore filename $fn
+    # if $unrepres is defined, array ref to which is appeneded
+    #  a list of unrepresentable changes (removals of upstream files
+    #  (as messages)
     local $/=undef;
-    my @cmd = (@git, qw(diff-tree --name-only -z));
-    push @cmd, qw(-r) if $finegrained;
+    my @cmd = (@git, qw(diff-tree -z));
+    push @cmd, qw(--name-only) unless $unrepres;
+    push @cmd, qw(-r) if $finegrained || $unrepres;
     push @cmd, $x, $y;
     my $diffs= cmdoutput @cmd;
     my $r = 0;
+    my @lmodes;
     foreach my $f (split /\0/, $diffs) {
+       if ($unrepres && !@lmodes) {
+           @lmodes = $f =~ m/^\:(\w+) (\w+) \w+ \w+ / or die "$_ ?";
+           next;
+       }
+       my ($oldmode,$newmode) = @lmodes;
+       @lmodes = ();
+
        next if $f =~ m#^debian(?:/.*)?$#s;
+
+       if ($unrepres) {
+           eval {
+               die "deleted\n" unless $newmode =~ m/[^0]/;
+               die "not a plain file\n" unless $newmode =~ m/^10\d{4}$/;
+               if ($oldmode =~ m/[^0]/) {
+                   die "mode changed\n" if $oldmode ne $newmode;
+               } else {
+                   die "non-default mode\n" unless $newmode =~ m/^100644$/;
+               }
+           };
+           if ($@) {
+               local $/="\n"; chomp $@;
+               push @$unrepres, [ $f, $@ ];
+           }
+       }
+
        my $isignore = $f =~ m#^(?:.*/)?.gitignore$#s;
        $r |= $isignore ? 02 : 01;
        $ignorenamesr->{$f}=1 if $ignorenamesr && $isignore;
@@ -3716,7 +3838,7 @@ sub quiltify_splitbrain ($$$$$$) {
     local $ENV{GIT_AUTHOR_DATE} =  $authline[2];
 
     if ($quilt_mode =~ m/gbp|unapplied/ &&
-       ($diffbits->{H2O} & 01)) {
+       ($diffbits->{O2H} & 01)) {
        my $msg =
  "--quilt=$quilt_mode specified, implying patches-unapplied git tree\n".
  " but git tree differs from orig in upstream files.";
@@ -3750,7 +3872,7 @@ END
  .gitignores: but, such patches exist in debian/patches.
 END
     }
-    if (($diffbits->{H2O} & 02) && # user has modified .gitignore
+    if (($diffbits->{O2H} & 02) && # user has modified .gitignore
        !($diffbits->{O2A} & 02)) { # patches do not change .gitignore
        quiltify_splitbrain_needed();
        progress "dgit view: creating patch to represent .gitignore changes";
@@ -3781,7 +3903,11 @@ END
        print SERIES "auto-gitignore\n" or die $!;
        close SERIES or die  $!;
         runcmd @git, qw(add -- debian/patches/series), $gipatch;
-        commit_admin "Commit patch to update .gitignore";
+        commit_admin <<END
+Commit patch to update .gitignore
+
+[dgit ($our_version) update-gitignore-quilt-fixup]
+END
     }
 
     my $dgitview = git_rev_parse 'refs/heads/dgit-view';
@@ -3976,15 +4102,75 @@ sub quiltify ($$$$) {
        $commitdata =~ m/^author (.*) \d+ [-+0-9]+$/m or die "$cc ?";
        my $author = $1;
 
+       my $commitdate = cmdoutput
+           @git, qw(log -n1 --pretty=format:%aD), $cc;
+
        $msg =~ s/^(.*)\n*/$1\n/ or die "$cc $msg ?";
 
+       my $strip_nls = sub { $msg =~ s/\n+$//; $msg .= "\n"; };
+       $strip_nls->();
+
        my $title = $1;
-       my $patchname = $title;
-       $patchname =~ s/[.:]$//;
-       $patchname =~ y/ A-Z/-a-z/;
-       $patchname =~ y/-a-z0-9_.+=~//cd;
-       $patchname =~ s/^\W/x-$&/;
-       $patchname = substr($patchname,0,40);
+       my $patchname;
+       my $patchdir;
+
+       my $gbp_check_suitable = sub {
+           $_ = shift;
+           my ($what) = @_;
+
+           eval {
+               die "contains unexpected slashes\n" if m{//} || m{/$};
+               die "contains leading punctuation\n" if m{^\W} || m{/\W};
+               die "contains bad character(s)\n" if m{[^-a-z0-9_.+=~/]}i;
+               die "too long" if length > 200;
+           };
+           return $_ unless $@;
+           print STDERR "quiltifying commit $cc:".
+               " ignoring/dropping Gbp-Pq $what: $@";
+           return undef;
+       };
+
+       if ($msg =~ s/^ (?: gbp(?:-pq)? : \s* name \s+ |
+                          gbp-pq-name: \s* )
+                      (\S+) \s* \n //ixm) {
+           $patchname = $gbp_check_suitable->($1, 'Name');
+       }
+       if ($msg =~ s/^ (?: gbp(?:-pq)? : \s* topic \s+ |
+                          gbp-pq-topic: \s* )
+                      (\S+) \s* \n //ixm) {
+           $patchdir = $gbp_check_suitable->($1, 'Topic');
+       }
+
+       $strip_nls->();
+
+       if (!defined $patchname) {
+           $patchname = $title;
+           $patchname =~ s/[.:]$//;
+            use Text::Iconv;
+           eval {
+               my $converter = new Text::Iconv qw(UTF-8 ASCII//TRANSLIT);
+               my $translitname = $converter->convert($patchname);
+               die unless defined $translitname;
+               $patchname = $translitname;
+           };
+           print STDERR
+               "dgit: patch title transliteration error: $@"
+               if $@;
+           $patchname =~ y/ A-Z/-a-z/;
+           $patchname =~ y/-a-z0-9_.+=~//cd;
+           $patchname =~ s/^\W/x-$&/;
+           $patchname = substr($patchname,0,40);
+       }
+       if (!defined $patchdir) {
+           $patchdir = '';
+       }
+       if (length $patchdir) {
+           $patchname = "$patchdir/$patchname";
+       }
+       if ($patchname =~ m{^(.*)/}) {
+           mkpath "debian/patches/$1";
+       }
+
        my $index;
        for ($index='';
             stat "debian/patches/$patchname$index";
@@ -4002,6 +4188,7 @@ sub quiltify ($$$$) {
        runcmd @git, qw(checkout -q), $target, qw(debian/changelog);
 
        quiltify_dpkg_commit "$patchname$index", $author, $msg,
+           "Date: $commitdate\n".
            "X-Dgit-Generated: $clogp->{Version} $cc\n";
 
        runcmd @git, qw(checkout -q), $cc, qw(debian/changelog);
@@ -4077,7 +4264,11 @@ sub quilt_fixup_linkorigs ($$) {
 
 sub quilt_fixup_delete_pc () {
     runcmd @git, qw(rm -rqf .pc);
-    commit_admin "Commit removal of .pc (quilt series tracking data)";
+    commit_admin <<END
+Commit removal of .pc (quilt series tracking data)
+
+[dgit ($our_version) upgrade quilt-remove-pc]
+END
 }
 
 sub quilt_fixup_singlepatch ($$$) {
@@ -4320,8 +4511,17 @@ sub quilt_fixup_multipatch ($$$) {
 
     ensuredir '.pc';
 
-    runcmd qw(sh -ec),
-        'exec dpkg-source --before-build . >/dev/null';
+    my @bbcmd = (qw(sh -ec), 'exec dpkg-source --before-build . >/dev/null');
+    $!=0; $?=-1;
+    if (system @bbcmd) {
+       failedcmd @bbcmd if $? < 0;
+       fail <<END;
+failed to apply your git tree's patch stack (from debian/patches/) to
+ the corresponding upstream tarball(s).  Your source tree and .orig
+ are probably too inconsistent.  dgit can only fix up certain kinds of
+ anomaly (depending on the quilt mode).  See --quilt= in dgit(1).
+END
+    }
 
     changedir '..';
 
@@ -4348,31 +4548,44 @@ sub quilt_fixup_multipatch ($$$) {
     # be.  This is mostly for error reporting.
 
     my %editedignores;
+    my @unrepres;
     my $diffbits = {
         # H = user's HEAD
         # O = orig, without patches applied
         # A = "applied", ie orig with H's debian/patches applied
-        H2O => quiltify_trees_differ($headref,  $unapplied, 1,\%editedignores),
+        O2H => quiltify_trees_differ($unapplied,$headref,   1,
+                                    \%editedignores, \@unrepres),
         H2A => quiltify_trees_differ($headref,  $oldtiptree,1),
         O2A => quiltify_trees_differ($unapplied,$oldtiptree,1),
     };
 
     my @dl;
     foreach my $b (qw(01 02)) {
-        foreach my $v (qw(H2O O2A H2A)) {
+        foreach my $v (qw(O2H O2A H2A)) {
             push @dl, ($diffbits->{$v} & $b) ? '##' : '==';
         }
     }
     printdebug "differences \@dl @dl.\n";
 
     progress sprintf
+"$us: base trees orig=%.20s o+d/p=%.20s",
+              $unapplied, $oldtiptree;
+    progress sprintf
 "$us: quilt differences: src:  %s orig %s     gitignores:  %s orig %s\n".
 "$us: quilt differences:      HEAD %s o+d/p               HEAD %s o+d/p",
                              $dl[0], $dl[1],              $dl[3], $dl[4],
                                  $dl[2],                     $dl[5];
 
+    if (@unrepres) {
+       print STDERR "dgit:  cannot represent change: $_->[1]: $_->[0]\n"
+           foreach @unrepres;
+       fail <<END;
+HEAD has changes to .orig[s] which are not representable by `3.0 (quilt)'
+END
+    }
+
     my @failsuggestion;
-    if (!($diffbits->{H2O} & $diffbits->{O2A})) {
+    if (!($diffbits->{O2H} & $diffbits->{O2A})) {
         push @failsuggestion, "This might be a patches-unapplied branch.";
     }  elsif (!($diffbits->{H2A} & $diffbits->{O2A})) {
         push @failsuggestion, "This might be a patches-applied branch.";
@@ -4611,6 +4824,10 @@ sub cmd_build {
     printdone "build successful\n";
 }
 
+sub pre_gbp_build {
+    $quilt_mode //= 'gbp';
+}
+
 sub cmd_gbp_build {
     my @dbp = @dpkgbuildpackage;
 
@@ -4831,8 +5048,6 @@ defvalopt '--build-products-dir','','.*',     \$buildproductsdir;
 defvalopt '--clean',       '', $cleanmode_re, \$cleanmode;
 defvalopt '--quilt',     '', $quilt_modes_re, \$quilt_mode;
 
-defvalopt '', '-c', '.*=.*', sub { push @git, '-c', @_; };
-
 defvalopt '', '-C', '.+', sub {
     ($changesfile) = (@_);
     if ($changesfile =~ s#^(.*)/##) {
@@ -4983,6 +5198,12 @@ sub parseopts () {
                } elsif (s/^-wc$//s) {
                    push @ropts, $&;
                    $cleanmode = 'check';
+               } elsif (s/^-c([^=]*)\=(.*)$//s) {
+                   push @git, '-c', $&;
+                   $gitcfgs{cmdline}{$1} = [ $2 ];
+               } elsif (s/^-c([^=]+)$//s) {
+                   push @git, '-c', $&;
+                   $gitcfgs{cmdline}{$1} = [ 'true' ];
                } elsif (m/^-[a-zA-Z]/ && ($oi = $valopts_short{$&})) {
                    $val = $'; #';
                    $val = undef unless length $val;
@@ -5008,16 +5229,18 @@ sub finalise_opts_opts () {
        }
 
        foreach my $c (access_cfg_cfgs("opts-$k")) {
-           my $vl = $gitcfg{$c};
-           printdebug "CL $c ",
-               ($vl ? join " ", map { shellquote } @$vl : ""),
+           my @vl =
+               map { $_ ? @$_ : () }
+               map { $gitcfgs{$_}{$c} }
+               reverse @gitcfgsources;
+           printdebug "CL $c ", (join " ", map { shellquote } @vl),
                "\n" if $debuglevel >= 4;
-           next unless $vl;
+           next unless @vl;
            badcfg "cannot configure options for $k"
                if $opts_opt_cmdonly{$k};
            my $insertpos = $opts_cfg_insertpos{$k};
            @$om = ( @$om[0..$insertpos-1],
-                    @$vl,
+                    @vl,
                     @$om[$insertpos..$#$om] );
        }
     }
@@ -5041,6 +5264,9 @@ if (!@ARGV) {
 my $cmd = shift @ARGV;
 $cmd =~ y/-/_/;
 
+my $pre_fn = ${*::}{"pre_$cmd"};
+$pre_fn->() if $pre_fn;
+
 if (!defined $rmchanges) {
     local $access_forpush;
     $rmchanges = access_cfg_bool(0, 'rm-old-changes');