chiark / gitweb /
Support --deliberately-not-fast-forward
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 21 Mar 2015 14:56:24 +0000 (14:56 +0000)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Sun, 22 Mar 2015 15:20:29 +0000 (15:20 +0000)
dgit
infra/dgit-repos-server

diff --git a/dgit b/dgit
index ad6289d53411a4ca155484dc48a1699e4ff6f056..9d40ceef5db54a2ae53c2b32ae4c5c0bd2a5110f 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -1572,6 +1572,12 @@ tagger $authline
 $package release $cversion for $clogsuite ($csuite) [dgit]
 [dgit distro=$distro$delibs]
 END
 $package release $cversion for $clogsuite ($csuite) [dgit]
 [dgit distro=$distro$delibs]
 END
+    foreach my $ref (sort keys %supersedes) {
+                   print TO <<END or die $!;
+[dgit supersede:$ref=$supersedes{$ref}]
+END
+    }
+
     close TO or die $!;
 
     my $tagobjfn = $tfn->('.tmp');
     close TO or die $!;
 
     my $tagobjfn = $tfn->('.tmp');
@@ -1684,6 +1690,15 @@ sub dopush () {
     responder_send_command("param head $head");
     responder_send_command("param csuite $csuite");
 
     responder_send_command("param head $head");
     responder_send_command("param csuite $csuite");
 
+    my $forceflag = deliberately('not-fast-forward') ? '+' : '';
+    if ($forceflag && defined $lastpush_hash) {
+       git_for_each_tag_referring($lastpush_hash, sub {
+           my ($objid,$fullrefname,$tagname) = @_;
+           responder_send_command("supersedes $fullrefname=$objid");
+           $supersedes{$fullrefname} = $objid;
+       });
+    }
+
     my $tfn = sub { ".git/dgit/tag$_[0]"; };
     my $tagobjfn;
 
     my $tfn = sub { ".git/dgit/tag$_[0]"; };
     my $tagobjfn;
 
@@ -1707,7 +1722,7 @@ sub dopush () {
        create_remote_git_repo();
     }
     runcmd_ordryrun @git, qw(push),access_giturl(),
        create_remote_git_repo();
     }
     runcmd_ordryrun @git, qw(push),access_giturl(),
-        "HEAD:".rrref(), "refs/tags/$tag";
+        $forceflag."HEAD:".rrref(), "refs/tags/$tag";
     runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
 
     if ($we_are_responder) {
     runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
 
     if ($we_are_responder) {
@@ -1988,6 +2003,14 @@ sub i_resp_param ($) {
     $i_param{$1} = $2;
 }
 
     $i_param{$1} = $2;
 }
 
+sub i_resp_supersedes ($) {
+    $_[0] =~ m#^(refs/tags/\S+)=(\w+)$#
+       or badproto \*RO, "bad supersedes spec";
+    my $r = system qw(git check-ref-format), $1;
+    die "bad supersedes ref spec ($r)" if $r;
+    $supersedes{$1} = $2;
+}
+
 our %i_wanted;
 
 sub i_resp_want ($) {
 our %i_wanted;
 
 sub i_resp_want ($) {
index d2f94f1957e0bed922a0db052e2a81b921fa789a..551efff9fb5f466ec716dbd0d951fd83040443dd 100755 (executable)
@@ -103,6 +103,7 @@ our $keyrings;
 our @lockfhs;
 our $debug='';
 our @deliberatelies;
 our @lockfhs;
 our $debug='';
 our @deliberatelies;
+our %supersedes;
 our $policy;
 
 #----- utilities -----
 our $policy;
 
 #----- utilities -----
@@ -211,12 +212,24 @@ sub mkrepo_fromtemplate ($) {
 }
 
 sub movetogarbage () {
 }
 
 sub movetogarbage () {
-    my $garbagerepo = "$dgitrepos/_tmp/${package}_garbage";
-    acquiretree($garbagerepo,1);
-    rmtree $garbagerepo;
+    my $garbagerepo = "$dgitrepos/${package}_garbage";
+    my $lfh =acquiretree($garbagerepo,1);
+    # We arrange to always keep at least one old tree, for anti-rewind
+    # purposes (and, I guess, recovery from mistakes).  This is either
+    # $garbage or $garbage-old.
+    if (stat_exists "$garbagerepo") {
+       rmtree "$garbagerepo-tmp";
+       if (rename "$garbagerepo-old", "$garbagerepo-tmp") {
+           rmtree "$garbagerepo-tmp";
+       } else {
+           die "$garbagerepo $!" unless $!==ENOENT;
+       }
+       rename "$garbagerepo", "$garbagerepo-old" or die "$garbagerepo $!";
+    }
     rename $realdestrepo, $garbagerepo
        or $! == ENOENT
     rename $realdestrepo, $garbagerepo
        or $! == ENOENT
-       or die "rename repo $realdestrepo to $garbagerepo: $!";
+       or die "$garbagerepo $!";
+    close $lfh;
 }
 
 sub onwardpush () {
 }
 
 sub onwardpush () {
@@ -391,6 +404,9 @@ sub parsetag () {
                    die "$1 != $distro" unless $1 eq $distro;
                } elsif (s/^(--deliberately-$package_re) //) {
                    push @deliberatelies, $1;
                    die "$1 != $distro" unless $1 eq $distro;
                } elsif (s/^(--deliberately-$package_re) //) {
                    push @deliberatelies, $1;
+               } elsif (s/^supersede:(\S+)=(\w+) //) {
+                   die "supersede $1 twice" if defined $supersedes{$1};
+                   $supersedes{$1} = $2;
                } elsif (s/^[-+.=0-9a-z]\S* //) {
                } else {
                    die "unknown dgit info in tag";
                } elsif (s/^[-+.=0-9a-z]\S* //) {
                } else {
                    die "unknown dgit info in tag";
@@ -520,6 +536,74 @@ sub checksuite () {
     reject "unknown suite";
 }
 
     reject "unknown suite";
 }
 
+sub checktagnoreplay () {
+    # We check that the signed tag mentions the name and value of
+    # (a) in the case of FRESHREPO all tags in the repo;
+    # (b) in the case of just NOFFCHECK all tags referring to
+    # the current head for the suite (there must be at least one).
+    # This prevents a replay attack using an earlier signed tag.
+    return unless $policy & (FRESHREPO|NOFFCHECK);
+
+    my $garbagerepo = "$dgitrepos/${package}_garbage";
+    acquiretree($garbagerepo,1);
+
+    local $ENV{GIT_DIR};
+    foreach my $garb ("$garbagerepo", "$garbagerepo-old") {
+       if (stat_exists $garb) {
+           $ENV{GIT_DIR} = $garb;
+           last;
+       }
+    }
+    if (!defined $ENV{GIT_DIR}) {
+       # Nothing to overwrite so the FRESHREPO and NOFFCHECK were
+       # pointless.  Oh well.
+       debug "checktagnoreplay - no garbage, ok";
+       return;
+    }
+
+    my $onlyreferring;
+    if (!($policy & FRESHREPO)) {
+       my $branch = server_branch($suite);
+       $!=0; $?=0; $_ =
+           `git for-each-ref --format='%(objectname)' '[r]efs/$branch'`;
+       defined or die "$branch $? $!";
+       $? and die "$branch $?";
+       if (!length) {
+           # No such branch - NOFFCHECK was unnecessary.  Oh well.
+           debug "checktagnoreplay - not FRESHREPO, new branch, ok";
+           return;
+       }
+       m/^(\w+)\n$/ or die "$branch $_ ?";
+        $onlyreferring = $1;
+       debug "checktagnoreplay - not FRESHREPO,".
+           " checking for overwriting refs/$branch=$onlyreferring";
+    }
+
+    my @problems;
+
+    git_for_each_tag_referring($objreferring, sub {
+       my ($objid,$fullrefname,$tagname) = @_;
+       debug "checktagnoreplay - overwriting $fullrefname=$objid";
+       my $supers = $supersedes{$fullrefname};
+       if (!defined $supers) {
+           push @problems, "does not supersede $fullrefname";
+       } elsif ($supers ne $objid) {
+           push @problems,
+ "supersedes $fullrefname=$supers but previously $fullrefname=$objid";
+       } else {
+           # ok;
+       }
+    });
+
+    if (@problems) {
+       reject "replay attack prevention check failed:".
+           " signed tag for $version: ".
+           join("; ", @problems).
+           "\n";
+    }
+    debug "checktagnoreply - all ok"
+}
+
 sub tagh1 ($) {
     my ($tag) = @_;
     my $vals = $tagh{$tag};
 sub tagh1 ($) {
     my ($tag) = @_;
     my $vals = $tagh{$tag};
@@ -545,6 +629,7 @@ sub checks () {
                         $version,$suite,$tagname,
                         join(",",@delberatelies));
 
                         $version,$suite,$tagname,
                         join(",",@delberatelies));
 
+    checktagnoreplay();
     checksuite();
 
     # check that our ref is being fast-forwarded
     checksuite();
 
     # check that our ref is being fast-forwarded