git-remote-entire: regression test for helper-status on send-pack error · Entire

git-remote-entire: regression test for helper-status on send-pack error

052ba33→main·

pjbgf·1mo ago·1 file·+71 added/-0 removed

handlePush relays send-pack's helper-status lines to stdout before checking the subprocess exit code, so per-ref rejections (branch protection, D/F conflict, server hook decline) reach git's transport-helper.c and render as ! [remote rejected] <ref> (<reason>). Pin that ordering: a PATH-injected shell stub plays the role of git send-pack, emits an empty wrapped request followed by a trailing flush + error <ref> ... helper-status line, and exits 1. The test asserts that handlePush both returns the exit error AND has already written the helper-status to stdout. Verified failing against the pre-fix push.go (helperStatus empty in stdout).

Assisted-by: Claude Opus 4.7 noreply@anthropic.com Signed-off-by: Paulo Gomes paulo@entire.io

Sessions

d814978a0c6cView transcript

Changes

1

// and every byte that reached stdout.

import (
    "bufio"
    "bytes"
    "context"
    "errors"
    "io"
    "os"
    "path/filepath"
    "runtime"
    "strings"
    "testing"
)

// TestInvariant_HelperStatusSurfacedOnSendPackError: when `git send-pack`
// exits non-zero on a per-ref rejection (e.g. branch protection, D/F
// conflict, server hook decline), its buffered `error <ref> <reason>`
// helper-status lines are what `transport-helper.c` reads to render
// `! [remote rejected] <ref> (<reason>)`. handlePush must therefore
// relay helper-status to stdout BEFORE returning the send-pack exit
// error — relaying only on the success path strips the rejection
// reason and leaves the user with bare "send-pack exited with error".
//
// A shell stub on PATH plays the role of `git send-pack`: it emits the
// minimal wire shape handlePush expects (empty wrapped request, then
// trailing flush + helper-status), and exits 1.
func TestInvariant_HelperStatusSurfacedOnSendPackError(t *testing.T) {
    // No t.Parallel(): t.Setenv("PATH", ...) mutates process-global state.
    if runtime.GOOS == "windows" {
        t.Skip("shell-script PATH stub is POSIX-only")
    }

ref := testRefMain
    helperStatusLine := "error " + ref + " remote rejected: branch protection violation\n"

// Stub git: emits the outer flush that terminates an empty
    // send-pack request, drains stdin (refspecs + advertisement +
    // receive-pack response), then emits the trailing flush +
    // helper-status line, then exits 1. The exit code mirrors
    // send-pack's behaviour on per-ref rejection.
    stubDir := t.TempDir()
    stubPath := filepath.Join(stubDir, "git")
    stub := "#!/bin/sh\n" +
        "printf '0000'\n" +
        "cat > /dev/null\n" +
        "printf '0000" + helperStatusLine + "'\n" +
        "exit 1\n"
    if err := os.WriteFile(stubPath, []byte(stub), 0o755); err != nil {
        t.Fatalf("writing stub git: %v", err)
    }
    t.Setenv("PATH", stubDir+string(os.PathListSeparator)+os.Getenv("PATH"))

oldSHA := strings.Repeat("a", 40)

ft := &fakeTransport{
        infoRefsResp: func() (io.ReadCloser, error) {
            return stringRC(serviceAnnouncement(serviceReceivePack,
            oldSHA+" "+ref+"\x00 report-status\n")), nil
        },
        serviceRPCResp: func(string, []byte) (io.ReadCloser, error) {
            return stringRC(""), nil
        },
    }

// handlePush takes the first "push" line plus a bufio.Reader for
    // the rest of the batch; readPushBatch terminates on the blank
    // line.
    firstLine := "push " + oldSHA + ":" + ref
    stdin := bufio.NewReader(strings.NewReader("\n"))

var stdout bytes.Buffer
    err := handlePush(context.Background(), ft, firstLine, &Options{}, stdin, &stdout)
    if err == nil {
        t.Fatal("expected error from send-pack exit 1")
    }
    if !strings.Contains(stdout.String(), helperStatusLine) {
        t.Errorf("stdout missing helper-status line; got %q, want substring %q",
            stdout.String(), helperStatusLine)
    }
}

Minternal/remotehelper/githelper/invariants_test.go+71