Merge pull request #14 from entireio/pkt-err · Entire

Merge pull request #14 from entireio/pkt-err

c10ec48→main

Improve git protocol parsing and error handling

Changes

8

100 unmodified lines
101
102
103
104
104
105
106
107

100 unmodified lines

- Optional machine-readable output with `--json`
- Optional source-side Git protocol v2 for `ls-refs` and `fetch`

## Limits
## Limitations

- Push still uses the existing v1-style `receive-pack` path.
- Protocol v2 support currently covers source discovery and source fetch only.

MREADME.md+1/-1

249 unmodified lines

func storeV2FetchPack(store storer.Storer, r io.Reader, verbose bool) error {
    reader := NewPacketReader(r)
    expectPackfile := false
    for {
        kind, payload, err := reader.ReadPacket()
        if err != nil {
            if errors.Is(err, io.EOF) {
                if expectPackfile {
                    return errors.New("expected packfile to be sent after 'ready'")
                }
                return nil
            }
            return fmt.Errorf("decode protocol v2 fetch response: %w", err)
        }
        switch kind {
        case PacketFlush:
            if expectPackfile {
                return errors.New("expected packfile to be sent after 'ready'")
            }
            return nil
        case PacketDelim, PacketResponseEnd:
            continue
        case PacketData:
            line := string(payload)
            if strings.HasPrefix(line, "ERR ") {
                return errors.New("remote: " + line[4:])
            }
            switch line {
            case "packfile\n":
                demux := sideband.NewDemuxer(sideband.Sideband64k, reader.BufReader())
                return fmt.Errorf("update object storage: %w", err)
            }
            return nil
        case "acknowledgments\n", "shallow-info\n":
            expectPackfile = true
            if err := SkipSection(reader); err != nil {
                return err
            }
        }
        continue
    }
}

MREADME.md+1/-1


249 unmodified lines

func TestDecodeV2LSRefsSkipsUnbornLines(t *testing.T) {
    wire := "" +
        FormatPktLine("unborn HEAD symref-target:refs/heads/main\n") +
        FormatPktLine("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main\n") +
        "0000"
    refs, head, err := decodeV2LSRefs(bytes.NewReader([]byte(wire)))
    if err != nil {
        t.Fatalf("decodeV2LSRefs: %v", err)
    }
    if len(refs) != 1 {
        t.Fatalf("expected 1 ref, got %d", len(refs))
    }
    if refs[0].Name().String() != refsHeadsMain {
        t.Errorf("refs[0].Name() = %q, want refs/heads/main", refs[0].Name())
    }
    if refs[0].Hash().IsZero() {
        t.Fatal("unborn line was decoded as a zero-hash ref")
    }
}

func TestDecodeV2LSRefsMalformed(t *testing.T) {
    wire := FormatPktLine("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n") + "0000"
}

func TestStoreV2FetchPackReturnsRemoteError(t *testing.T) {
    var wire bytes.Buffer
    if _, err := pktline.WriteString(&wire, "ERR upload-pack: not our ref"); err != nil {
        t.Fatalf("write remote error: %v", err)
    }

err := storeV2FetchPack(memory.NewStorage(), &wire, false)
    if err == nil {
        t.Fatal("expected remote error")
    }
    if got, want := err.Error(), "remote: upload-pack: not our ref"; got != want {
        t.Fatalf("error = %q, want %q", got, want)
    }
}

func TestOpenV2PackStreamReturnsRemoteError(t *testing.T) {
    var wire bytes.Buffer
    if _, err := pktline.WriteString(&wire, "ERR upload-pack: not our ref"); err != nil {
        t.Fatalf("write remote error: %v", err)
    }

_, err := openV2PackStream(io.NopCloser(&wire), false)
    if err == nil {
        t.Fatal("expected remote error")
    }
    if got, want := err.Error(), "remote: upload-pack: not our ref"; got != want {
        t.Fatalf("error = %q, want %q", got, want)
    }
}

func TestFetchPackV2ManyHavesSendsDoneAndReadsPack(t *testing.T) {
    const negotiationBoundary = 16
    const refCount = negotiationBoundary + 1

ep, err := transport.ParseURL("https://example.com/repo.git")
    if err != nil {
        t.Fatalf("parse endpoint: %v", err)
    }

seenRequest := false
    conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
        seenRequest = true
        body, err := io.ReadAll(req.Body)
        if err != nil {
            t.Fatalf("read request body: %v", err)
        }
        lines := readV2FetchRequestLines(t, body)
        if got := countLinesWithPrefix(lines, "want "); got != refCount {
            t.Fatalf("want lines = %d, want %d; lines=%q", got, refCount, lines)
        }
        if got := countLinesWithPrefix(lines, "have "); got != refCount {
            t.Fatalf("have lines = %d, want %d; lines=%q", got, refCount, lines)
        }
        if !containsLine(lines, "done") {
            t.Fatalf("fetch request did not send done; lines=%q", lines)
        }

var wire bytes.Buffer
        if _, err := pktline.WriteString(&wire, "packfile\n"); err != nil {
            t.Fatalf("write packfile header: %v", err)
        }
        if _, err := pktline.Write(&wire, append([]byte{1}, []byte("PACK")...)); err != nil {
            t.Fatalf("write sideband packet: %v", err)
        }
        if err := pktline.WriteFlush(&wire); err != nil {
            t.Fatalf("write flush: %v", err)
        }
        return &http.Response{
            StatusCode: http.StatusOK,
            Request:    req,
            Body:       io.NopCloser(bytes.NewReader(wire.Bytes())),
        }, nil
    }))

}