chiark / gitweb /
dgit: Check that entirely-new uploads to Debian are not source-only-uploads.
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 2cf78ec7274e39d8c8fada21f8b0155c5be827e5..3a737d0b0bbd607dc002542fdd9f582bf14edfaa 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -90,6 +90,7 @@ our $chase_dsc_distro=1;
 our %forceopts = map { $_=>0 }
     qw(unrepresentable unsupported-source-format
        dsc-changes-mismatch changes-origs-exactly
+       uploading-binaries uploading-source-only
        import-gitapply-absurd
        import-gitapply-no-absurd
        import-dsc-with-dgit-field);
@@ -614,9 +615,11 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit.default.sshpsql-dbname' => 'service=projectb',
               'dgit.default.aptget-components' => 'main',
               'dgit.default.dgit-tag-format' => 'new,old,maint',
+              'dgit.default.source-only-uploads' => 'ok',
               'dgit.dsc-url-proto-ok.http'    => 'true',
               'dgit.dsc-url-proto-ok.https'   => 'true',
               'dgit.dsc-url-proto-ok.git'     => 'true',
+              'dgit.vcs-git.suites',          => 'sid', # ;-separated
               'dgit.default.dsc-url-proto-ok' => 'false',
               # old means "repo server accepts pushes with old dgit tags"
               # new means "repo server accepts pushes with new dgit tags"
@@ -627,6 +630,7 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit-distro.debian.git-check' => 'url',
               'dgit-distro.debian.git-check-suffix' => '/info/refs',
               'dgit-distro.debian.new-private-pushers' => 't',
+              'dgit-distro.debian.source-only-uploads' => 'not-wholly-new',
               'dgit-distro.debian/push.git-url' => '',
               'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
               'dgit-distro.debian/push.git-user-force' => 'dgit',
@@ -1178,6 +1182,12 @@ sub file_in_archive_ftpmasterapi {
     my $info = api_query($data, "file_in_archive/$pat", 1);
 }
 
+sub package_not_wholly_new_ftpmasterapi {
+    my ($proto,$data,$pkg) = @_;
+    my $info = api_query($data,"madison?package=${pkg}&f=json");
+    return !!@$info;
+}
+
 #---------- `aptget' archive query method ----------
 
 our $aptget_base;
@@ -1341,34 +1351,55 @@ sub archive_query_aptget {
 }
 
 sub file_in_archive_aptget () { return undef; }
+sub package_not_wholly_new_aptget () { return undef; }
 
 #---------- `dummyapicat' archive query method ----------
 
 sub archive_query_dummycatapi { archive_query_ftpmasterapi @_; }
 sub canonicalise_suite_dummycatapi { canonicalise_suite_ftpmasterapi @_; }
 
-sub file_in_archive_dummycatapi ($$$) {
-    my ($proto,$data,$filename) = @_;
+sub dummycatapi_run_in_mirror ($@) {
+    # runs $fn with FIA open onto rune
+    my ($rune, $argl, $fn) = @_;
+
     my $mirror = access_cfg('mirror');
     $mirror =~ s#^file://#/# or die "$mirror ?";
-    my @out;
-    my @cmd = (qw(sh -ec), '
-            cd "$1"
-            find -name "$2" -print0 |
-            xargs -0r sha256sum
-        ', qw(x), $mirror, $filename);
+    my @cmd = (qw(sh -ec), 'cd "$1"; shift'."\n".$rune,
+              qw(x), $mirror, @$argl);
     debugcmd "-|", @cmd;
     open FIA, "-|", @cmd or die $!;
-    while (<FIA>) {
-       chomp or die;
-       printdebug "| $_\n";
-       m/^(\w+)  (\S+)$/ or die "$_ ?";
-       push @out, { sha256sum => $1, filename => $2 };
-    }
-    close FIA or die failedcmd @cmd;
+    my $r = $fn->();
+    close FIA or ($!==0 && $?==141) or die failedcmd @cmd;
+    return $r;
+}
+
+sub file_in_archive_dummycatapi ($$$) {
+    my ($proto,$data,$filename) = @_;
+    my @out;
+    dummycatapi_run_in_mirror '
+            find -name "$1" -print0 |
+            xargs -0r sha256sum
+    ', [$filename], sub {
+       while (<FIA>) {
+           chomp or die;
+           printdebug "| $_\n";
+           m/^(\w+)  (\S+)$/ or die "$_ ?";
+           push @out, { sha256sum => $1, filename => $2 };
+       }
+    };
     return \@out;
 }
 
+sub package_not_wholly_new_dummycatapi {
+    my ($proto,$data,$pkg) = @_;
+    dummycatapi_run_in_mirror "
+            find -name ${pkg}_*.dsc
+    ", [], sub {
+       local $/ = undef;
+       !!<FIA>;
+    };
+}
+
 #---------- `madison' archive query method ----------
 
 sub archive_query_madison {
@@ -1419,6 +1450,7 @@ sub canonicalise_suite_madison {
 }
 
 sub file_in_archive_madison { return undef; }
+sub package_not_wholly_new_madison { return undef; }
 
 #---------- `sshpsql' archive query method ----------
 
@@ -1496,6 +1528,7 @@ END
 }
 
 sub file_in_archive_sshpsql ($$$) { return undef; }
+sub package_not_wholly_new_sshpsql ($$$) { return undef; }
 
 #---------- `dummycat' archive query method ----------
 
@@ -1540,6 +1573,7 @@ sub archive_query_dummycat ($$) {
 }
 
 sub file_in_archive_dummycat () { return undef; }
+sub package_not_wholly_new_dummycat () { return undef; }
 
 #---------- tag format handling ----------
 
@@ -1974,12 +2008,12 @@ END
            if ($found_same) {
                # in archive, delete from .changes if it's there
                $changed{$file} = "removed" if
-                   $changes->{$fname} =~ s/^.* \Q$file\E$(?:)\n//m;
-           } elsif ($changes->{$fname} =~ m/^.* \Q$file\E$(?:)\n/m) {
+                   $changes->{$fname} =~ s/\n.* \Q$file\E$(?:)$//m;
+           } elsif ($changes->{$fname} =~ m/^.* \Q$file\E$(?:)$/m) {
                # not in archive, but it's here in the .changes
            } else {
                my $dsc_data = getfield $dsc, $fname;
-               $dsc_data =~ m/^(.* \Q$file\E$)\n/m or die "$dsc_data $file ?";
+               $dsc_data =~ m/^(.* \Q$file\E$)$/m or die "$dsc_data $file ?";
                my $extra = $1;
                $extra =~ s/ \d+ /$&$placementinfo /
                    or die "$fname $extra >$dsc_data< ?"
@@ -2757,6 +2791,11 @@ END
            my $want = $wantr{$rrefname};
            next if $got eq $want;
            if (!defined $objgot{$want}) {
+               fail <<END unless act_local();
+--dry-run specified but we actually wanted the results of git fetch,
+so this is not going to work.  Try running dgit fetch first,
+or using --damp-run instead of --dry-run.
+END
                print STDERR <<END;
 warning: git ls-remote suggests we want $lrefname
 warning:  and it should refer to $want
@@ -2845,15 +2884,14 @@ sub mergeinfo_version ($) {
 
 sub fetch_from_archive_record_1 ($) {
     my ($hash) = @_;
-    runcmd @git, qw(update-ref -m), "dgit fetch $csuite",
-           'DGIT_ARCHIVE', $hash;
+    runcmd git_update_ref_cmd "dgit fetch $csuite", 'DGIT_ARCHIVE', $hash;
     cmdoutput @git, qw(log -n2), $hash;
     # ... gives git a chance to complain if our commit is malformed
 }
 
 sub fetch_from_archive_record_2 ($) {
     my ($hash) = @_;
-    my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
+    my @upd_cmd = (git_update_ref_cmd 'dgit fetch', lrref(), $hash);
     if (act_local()) {
        cmdoutput @upd_cmd;
     } else {
@@ -3552,7 +3590,7 @@ sub fork_for_multisuite ($) {
        my $csubsuite = multisuite_suite_child($tsuite, \@mergeinputs,
                                               sub {
             @end = ();
-            fetch();
+            fetch_one();
            finish 0;
        });
        # xxx collecte the ref here
@@ -3710,19 +3748,37 @@ sub clone ($) {
     clone_finish($dstdir);
 }
 
-sub fetch () {
+sub fetch_one () {
     canonicalise_suite();
     if (check_for_git()) {
        git_fetch_us();
     }
     fetch_from_archive() or no_such_package();
+    
+    my $vcsgiturl = $dsc && $dsc->{'Vcs-Git'};
+    if (length $vcsgiturl and
+       (grep { $csuite eq $_ }
+        split /\;/,
+        cfg 'dgit.vcs-git.suites')) {
+       my $current = cfg 'remote.vcs-git.url', 'RETURN-UNDEF';
+       if (defined $current && $current ne $vcsgiturl) {
+           print STDERR <<END;
+FYI: Vcs-Git in $csuite has different url to your vcs-git remote.
+ Your vcs-git remote url may be out of date.  Use dgit update-vcs-git ?
+END
+       }
+    }
     printdone "fetched into ".lrref();
 }
 
-sub pull () {
+sub dofetch () {
     my $multi_fetched = fork_for_multisuite(sub { });
-    fetch() unless $multi_fetched; # parent
-    return if $multi_fetched eq '0'; # child
+    fetch_one() unless $multi_fetched; # parent
+    finish 0 if $multi_fetched eq '0'; # child
+}
+
+sub pull () {
+    dofetch();
     runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
         lrref();
     printdone "fetched to ".lrref()." and merged into HEAD";
@@ -3760,7 +3816,7 @@ sub commit_quilty_patch () {
     my %adds;
     foreach my $l (split /\n/, $output) {
        next unless $l =~ m/\S/;
-       if ($l =~ m{^(?:\?\?| M) (.pc|debian/patches)}) {
+       if ($l =~ m{^(?:\?\?| [MADRC]) (.pc|debian/patches)}) {
            $adds{$1}++;
        }
     }
@@ -3830,7 +3886,7 @@ sub maybe_split_brain_save ($$$) {
     # => message fragment "$saved" describing disposition of $dgitview
     return "commit id $dgitview" unless defined $split_brain_save;
     my @cmd = (shell_cmd 'cd "$1"; shift', $maindir,
-              @git, qw(update-ref -m),
+              git_update_ref_cmd
               "dgit --dgit-view-save $msg HEAD=$headref",
               $split_brain_save, $dgitview);
     runcmd @cmd;
@@ -3994,10 +4050,12 @@ sub splitbrain_pseudomerge ($$$$) {
        infopair_cond_ff($i_dep14, [ $maintview, 'HEAD' ]);
        1;
     }) {
+        $@ =~ s/^\n//; chomp $@;
        print STDERR <<END;
-$us: check failed (maybe --overwrite is needed, consult documentation)
+$@
+| Not fast forward; maybe --overwrite is needed, see dgit(1)
 END
-       die "$@";
+       finish -1;
     }
 
     my $r = pseudomerge_make_commit
@@ -4030,7 +4088,7 @@ sub plain_overwrite_pseudomerge ($$$) {
        $clogp, $head, $archive_hash, $i_arch_v,
        "dgit", $m;
 
-    runcmd @git, qw(update-ref -m), $m, 'HEAD', $r, $head;
+    runcmd git_update_ref_cmd $m, 'HEAD', $r, $head;
 
     progress "Make pseudo-merge of $i_arch_v->[0] into your HEAD.";
     return $r;
@@ -4296,7 +4354,8 @@ END
        }
     }
 
-    if (defined $overwrite_version && !defined $maintviewhead) {
+    if (defined $overwrite_version && !defined $maintviewhead
+       && $archive_hash) {
        $dgithead = plain_overwrite_pseudomerge($clogp,
                                                $dgithead,
                                                $archive_hash);
@@ -4392,6 +4451,29 @@ END
     files_compare_inputs($dsc, $changes)
        unless forceing [qw(dsc-changes-mismatch)];
 
+    # Check whether this is a source only upload
+    my $hasdebs = $changes->{Files} =~ m{\.deb$}m;
+    my $sourceonlypolicy = access_cfg 'source-only-uploads';
+    if ($sourceonlypolicy eq 'ok') {
+    } elsif ($sourceonlypolicy eq 'always') {
+       forceable_fail [qw(uploading-binaries)],
+           "uploading binaries, although distroy policy is source only"
+           if $hasdebs;
+    } elsif ($sourceonlypolicy eq 'never') {
+       forceable_fail [qw(uploading-source-only)],
+           "source-only upload, although distroy policy requires .debs"
+           if !$hasdebs;
+    } elsif ($sourceonlypolicy eq 'not-wholly-new') {
+       forceable_fail [qw(uploading-source-only)],
+           "source-only upload, even though package is entirely NEW\n".
+           "(this is contrary to policy in ".(access_nomdistro()).")"
+           if !$hasdebs
+           && $new_package
+           && !(archive_query('package_not_wholly_new', $package) // 1);
+    } else {
+       badcfg "unknown source-only-uploads policy \`$sourceonlypolicy'";
+    }
+
     # Perhaps adjust .dsc to contain right set of origs
     changes_update_origs_from_dsc($dsc, $changes, $upstreamversion,
                                  $changesfile)
@@ -4475,7 +4557,7 @@ END
 
     runcmd_ordryrun @git,
        qw(-c push.followTags=false push), access_giturl(), @pushrefs;
-    runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), $dgithead;
+    runcmd_ordryrun git_update_ref_cmd 'dgit push', lrref(), $dgithead;
 
     supplementary_message(<<'END');
 Push failed, while obtaining signatures on the .changes and .dsc.
@@ -4575,11 +4657,15 @@ sub branchsuite () {
     }
 }
 
-sub fetchpullargs () {
+sub package_from_d_control () {
     if (!defined $package) {
        my $sourcep = parsecontrol('debian/control','debian/control');
        $package = getfield $sourcep, 'Source';
     }
+}
+
+sub fetchpullargs () {
+    package_from_d_control();
     if (@ARGV==0) {
        $isuite = branchsuite();
        if (!$isuite) {
@@ -4598,9 +4684,7 @@ sub fetchpullargs () {
 sub cmd_fetch {
     parseopts();
     fetchpullargs();
-    my $multi_fetched = fork_for_multisuite(sub { });
-    finish 0 if $multi_fetched;
-    fetch();
+    dofetch();
 }
 
 sub cmd_pull {
@@ -4615,6 +4699,86 @@ END
     pull();
 }
 
+sub cmd_checkout {
+    parseopts();
+    package_from_d_control();
+    @ARGV==1 or badusage "dgit checkout needs a suite argument";
+    ($isuite) = @ARGV;
+    notpushing();
+
+    foreach my $canon (qw(0 1)) {
+       if (!$canon) {
+           $csuite= $isuite;
+       } else {
+           undef $csuite;
+           canonicalise_suite();
+       }
+       if (length git_get_ref lref()) {
+           # local branch already exists, yay
+           last;
+       }
+       if (!length git_get_ref lrref()) {
+           if (!$canon) {
+               # nope
+               next;
+           }
+           dofetch();
+       }
+       # now lrref exists
+       runcmd (@git, qw(update-ref), lref(), lrref(), '');
+       last;
+    }
+    local $ENV{GIT_REFLOG_ACTION} = git_reflog_action_msg
+        "dgit checkout $isuite";
+    runcmd (@git, qw(checkout), lbranch());
+}
+
+sub cmd_update_vcs_git () {
+    my $specsuite;
+    if (@ARGV==0 || $ARGV[0] =~ m/^-/) {
+       ($specsuite,) = split /\;/, cfg 'dgit.vcs-git.suites';
+    } else {
+       ($specsuite) = (@ARGV);
+       shift @ARGV;
+    }
+    my $dofetch=1;
+    if (@ARGV) {
+       if ($ARGV[0] eq '-') {
+           $dofetch = 0;
+       } elsif ($ARGV[0] eq '-') {
+           shift;
+       }
+    }
+
+    package_from_d_control();
+    my $ctrl;
+    if ($specsuite eq '.') {
+       $ctrl = parsecontrol 'debian/control', 'debian/control';
+    } else {
+       $isuite = $specsuite;
+       get_archive_dsc();
+       $ctrl = $dsc;
+    }
+    my $url = getfield $ctrl, 'Vcs-Git';
+
+    my @cmd;
+    my $orgurl = cfg 'remote.vcs-git.url', 'RETURN-UNDEF';
+    if (!defined $orgurl) {
+       print STDERR "setting up vcs-git: $url\n";
+       @cmd = (@git, qw(remote add vcs-git), $url);
+    } elsif ($orgurl eq $url) {
+       print STDERR "vcs git already configured: $url\n";
+    } else {
+       print STDERR "changing vcs-git url to: $url\n";
+       @cmd = (@git, qw(remote set-url vcs-git), $url);
+    }
+    runcmd_ordryrun_local @cmd;
+    if ($dofetch) {
+       print "fetching (@ARGV)\n";
+       runcmd_ordryrun_local @git, qw(fetch vcs-git), @ARGV;
+    }
+}
+
 sub prep_push () {
     parseopts();
     build_or_push_prep_early();
@@ -5062,13 +5226,15 @@ sub quiltify_splitbrain_needed () {
     }
 }
 
-sub quiltify_splitbrain ($$$$$$) {
-    my ($clogp, $unapplied, $headref, $diffbits,
+sub quiltify_splitbrain ($$$$$$$) {
+    my ($clogp, $unapplied, $headref, $oldtiptree, $diffbits,
        $editedignores, $cachekey) = @_;
+    my $gitignore_special = 1;
     if ($quilt_mode !~ m/gbp|dpm/) {
        # treat .gitignore just like any other upstream file
        $diffbits = { %$diffbits };
        $_ = !!$_ foreach values %$diffbits;
+       $gitignore_special = 0;
     }
     # We would like any commits we generate to be reproducible
     my @authline = clogp_authline($clogp);
@@ -5079,11 +5245,19 @@ sub quiltify_splitbrain ($$$$$$) {
     local $ENV{GIT_AUTHOR_EMAIL} = $authline[1];
     local $ENV{GIT_AUTHOR_DATE} =  $authline[2];
 
+    my $fulldiffhint = sub {
+       my ($x,$y) = @_;
+       my $cmd = "git diff $x $y -- :/ ':!debian'";
+       $cmd .= " ':!/.gitignore' ':!*/.gitignore'" if $gitignore_special;
+       return "\nFor full diff showing the problem(s), type:\n $cmd\n";
+    };
+
     if ($quilt_mode =~ m/gbp|unapplied/ &&
        ($diffbits->{O2H} & 01)) {
        my $msg =
  "--quilt=$quilt_mode specified, implying patches-unapplied git tree\n".
  " but git tree differs from orig in upstream files.";
+       $msg .= $fulldiffhint->($unapplied, 'HEAD');
        if (!stat_exists "debian/patches") {
            $msg .=
  "\n ... debian/patches is missing; perhaps this is a patch queue branch?";
@@ -5092,7 +5266,7 @@ sub quiltify_splitbrain ($$$$$$) {
     }
     if ($quilt_mode =~ m/dpm/ &&
        ($diffbits->{H2A} & 01)) {
-       fail <<END;
+       fail <<END. $fulldiffhint->($oldtiptree,'HEAD');
 --quilt=$quilt_mode specified, implying patches-applied git tree
  but git tree differs from result of applying debian/patches to upstream
 END
@@ -5108,7 +5282,7 @@ END
     }
     if ($quilt_mode =~ m/gbp|dpm/ &&
        ($diffbits->{O2A} & 02)) {
-       fail <<END
+       fail <<END;
 --quilt=$quilt_mode specified, implying that HEAD is for use with a
  tool which does not create patches for changes to upstream
  .gitignores: but, such patches exist in debian/patches.
@@ -5316,12 +5490,13 @@ sub quiltify ($$$$) {
            return $s;
        };
        if ($quilt_mode eq 'linear') {
-           print STDERR "$us: quilt fixup cannot be linear.  Stopped at:\n";
+           print STDERR "\n$us: error: quilt fixup cannot be linear.  Stopped at:\n";
            foreach my $notp (@nots) {
                print STDERR "$us:  ", $reportnot->($notp), "\n";
            }
            print STDERR "$us: $_\n" foreach @$failsuggestion;
-           fail "quilt fixup naive history linearisation failed.\n".
+           fail
+ "quilt history linearisation failed.  Search \`quilt fixup' in dgit(7).\n".
  "Use dpkg-source --commit by hand; or, --quilt=smash for one ugly patch";
        } elsif ($quilt_mode eq 'smash') {
        } elsif ($quilt_mode eq 'auto') {
@@ -5491,8 +5666,9 @@ END
                      make-patches --quiet-would-amend));
        # We tolerate soe snags that gdr wouldn't, by default.
        if (act_local()) {
+           debugcmd "+",@cmd;
            $!=0; $?=-1;
-           failedcmd @cmd if system @cmd and $?!=7;
+           failedcmd @cmd if system @cmd and $?!=7*256;
        } else {
            dryrun_report @cmd;
        }
@@ -5876,7 +6052,7 @@ END
         " --[quilt=]gbp --[quilt=]dpm --quilt=unapplied ?";
 
     if (quiltmode_splitbrain()) {
-       quiltify_splitbrain($clogp, $unapplied, $headref,
+       quiltify_splitbrain($clogp, $unapplied, $headref, $oldtiptree,
                             $diffbits, \%editedignores,
                            $splitbrain_cachekey);
        return;
@@ -6018,13 +6194,21 @@ sub changesopts_initial () {
 
 sub changesopts_version () {
     if (!defined $changes_since_version) {
-       my @vsns = archive_query('archive_query');
-       my @quirk = access_quirk();
-       if ($quirk[0] eq 'backports') {
-           local $isuite = $quirk[2];
-           local $csuite;
-           canonicalise_suite();
-           push @vsns, archive_query('archive_query');
+       my @vsns;
+       unless (eval {
+           @vsns = archive_query('archive_query');
+           my @quirk = access_quirk();
+           if ($quirk[0] eq 'backports') {
+               local $isuite = $quirk[2];
+               local $csuite;
+               canonicalise_suite();
+               push @vsns, archive_query('archive_query');
+           }
+           1;
+       }) {
+           print STDERR $@;
+           fail
+ "archive query failed (queried because --since-version not specified)";
        }
        if (@vsns) {
            @vsns = map { $_->[0] } @vsns;
@@ -6350,7 +6534,7 @@ sub cmd_quilt_fixup {
 
 sub import_dsc_result {
     my ($dstref, $newhash, $what_log, $what_msg) = @_;
-    my @cmd = (@git, qw(update-ref -m), $what_log, $dstref, $newhash);
+    my @cmd = (git_update_ref_cmd $what_log, $dstref, $newhash);
     runcmd @cmd;
     check_gitattrs($newhash, "source tree");