diff --git a/knotserver/git.go b/knotserver/git.go index b5d07903..8e8a7096 100644 --- a/knotserver/git.go +++ b/knotserver/git.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "os" + "os/exec" "path/filepath" "strings" @@ -398,11 +399,16 @@ func (h *Knot) ReceivePack(w http.ResponseWriter, r *http.Request) { writer: io.MultiWriter(w, pw), } + stdin := io.MultiReader(bytes.NewReader(prefix), bodyReader) + if !receivePackNeedsPackfile(repo.path, parsed) { + stdin = bytes.NewReader(prefix) + } + cmd := service.ServiceCommand{ GitProtocol: r.Header.Get("Git-Protocol"), Dir: repo.path, Stdout: capturingStdout, - Stdin: io.MultiReader(bytes.NewReader(prefix), bodyReader), + Stdin: stdin, GitConfig: []string{"core.hooksPath=/dev/null"}, Sandbox: h.sandbox, } @@ -571,3 +577,17 @@ func readReceivePackPrefix(bodyReader io.Reader) ([]byte, git.ReceivePackRequest } return buf.Bytes(), parsed, nil } + +func receivePackNeedsPackfile(repoPath string, parsed git.ReceivePackRequest) bool { + for _, line := range parsed.Lines { + if line.NewSha.IsZero() { + continue + } + cmd := exec.Command("git", "cat-file", "-e", line.NewSha.String()+"^{object}") + cmd.Dir = repoPath + if err := cmd.Run(); err != nil { + return true + } + } + return false +} diff --git a/knotserver/git_receive_pack_test.go b/knotserver/git_receive_pack_test.go index 22eddb2c..73a491d9 100644 --- a/knotserver/git_receive_pack_test.go +++ b/knotserver/git_receive_pack_test.go @@ -16,6 +16,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/go-chi/chi/v5" "tangled.org/core/dbdrv" @@ -690,7 +691,7 @@ func TestReceivePackStreamsLargePackfileWithoutBuffering(t *testing.T) { installFakeGit(t, receivePackReportStatus("refs/heads/main"), 0) const packSize = 16 * 1024 * 1024 - prefix := receivePackRequestBody("refs/heads/main") + prefix := receivePackRequestBodyFrom(strings.Repeat("1", 40), strings.Repeat("2", 40), "refs/heads/main") body := &streamingPackReader{prefix: prefix, packBytes: packSize} req := httptest.NewRequest(http.MethodPost, "/"+testOwnerDid+"/"+testRepoName+"/git-receive-pack", body) @@ -808,6 +809,47 @@ func TestReceivePackWithoutReportStatusEmitsAllRequestedRefs(t *testing.T) { } } +func TestReceivePackKnownObjectUpdateDoesNotWaitForBodyEOF(t *testing.T) { + // Why: smart-HTTP clients may keep the request body open while waiting for + // the receive-pack response when the update needs no packfile. If the new + // object already exists locally, the handler must feed only the parsed + // command prefix to git and close stdin; otherwise receive-pack can update + // refs but hang before post-receive side effects run. + h, commits := newHTTPSPushFixtureWithCommits(t, true) + installFakeGit(t, receivePackReportStatus("refs/heads/main"), 0) + + body := &blockingAfterReader{ + data: receivePackRequestBodyFrom(commits[0], commits[1], "refs/heads/main"), + } + req := httptest.NewRequest(http.MethodPost, "/"+testOwnerDid+"/"+testRepoName+"/git-receive-pack", body) + req.Header.Set("Content-Type", "application/x-git-receive-pack-request") + req = requestWithRepoParams(req, testActorDid) + + rec := httptest.NewRecorder() + done := make(chan struct{}) + go func() { + h.ReceivePack(rec, req) + close(done) + }() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("ReceivePack blocked waiting for request body EOF after a known-object update") + } + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d; body=%q", rec.Code, http.StatusOK, rec.Body.String()) + } + events, err := h.db.GetEvents(0, 1000) + if err != nil { + t.Fatalf("GetEvents: %v", err) + } + if len(events) != 1 { + t.Fatalf("len(events) = %d, want 1 (known-object no-pack update must fire post-receive)", len(events)) + } +} + func TestReceivePackContextPostReceiveErrorsDoNotFailPush(t *testing.T) { h := newHTTPSPushFixture(t, true) installFakeGit(t, receivePackReportStatus("refs/heads/main"), 0) @@ -822,3 +864,17 @@ func TestReceivePackContextPostReceiveErrorsDoNotFailPush(t *testing.T) { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } } + +type blockingAfterReader struct { + data []byte + off int +} + +func (r *blockingAfterReader) Read(p []byte) (int, error) { + if r.off < len(r.data) { + n := copy(p, r.data[r.off:]) + r.off += n + return n, nil + } + select {} +}