diff --git a/Justfile b/Justfile index a052491..7681423 100644 --- a/Justfile +++ b/Justfile @@ -29,7 +29,7 @@ lint: fmt: just --unstable --fmt git ls-files | grep '\.go$' | xargs gosimports -local github.com/block -w - go fmt ./... + git ls-files | grep '\.go$' | xargs dirname | uniq | sed 's,^,./,' | xargs go fmt go mod tidy # ============================================================================ @@ -61,10 +61,9 @@ build-all: # Run # ============================================================================ -# Run natively -run: build - @echo "→ Starting cachew at http://localhost:8080" - proctor +# Run dev server with hot reload +dev: + @proctor # Clean up build artifacts clean: diff --git a/Procfile b/Procfile index 2fe4ea7..4fc86de 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -cachewd **/*.go !**/*_test.go debounce=2s ready=http:8080/_readiness=200: CACHEW_URL=http://localhost:8080 CACHEW_LOG_LEVEL=debug cachewd +cachewd **/*.go !**/*_test.go !state/**/* debounce=2s ready=http:8080/_readiness=200: CACHEW_URL=http://localhost:8080 CACHEW_LOG_LEVEL=debug cachewd diff --git a/bin/.proctor-0.5.0.pkg b/bin/.proctor-0.8.0.pkg similarity index 100% rename from bin/.proctor-0.5.0.pkg rename to bin/.proctor-0.8.0.pkg diff --git a/bin/proctor b/bin/proctor index 946fc35..99be911 120000 --- a/bin/proctor +++ b/bin/proctor @@ -1 +1 @@ -.proctor-0.5.0.pkg \ No newline at end of file +.proctor-0.8.0.pkg \ No newline at end of file diff --git a/cachew.hcl b/cachew.hcl index dd0ef91..492de12 100644 --- a/cachew.hcl +++ b/cachew.hcl @@ -23,8 +23,8 @@ metrics {} git { - bundle-interval = "24h" - snapshot-interval = "24h" + #bundle-interval = "24h" + #snapshot-interval = "24h" } host "https://w3.org" {} diff --git a/internal/gitclone/manager.go b/internal/gitclone/manager.go index 67e22c3..c05ef3d 100644 --- a/internal/gitclone/manager.go +++ b/internal/gitclone/manager.go @@ -365,6 +365,13 @@ func (r *Repository) executeClone(ctx context.Context) error { return errors.Wrapf(err, "fetch all branches: %s", string(output)) } + // Enable partial clone support (e.g. --filter=blob:none) when serving via git http-backend. + cmd = exec.CommandContext(ctx, "git", "-C", r.path, "config", "uploadpack.allowFilter", "true") // #nosec G204 + output, err = cmd.CombinedOutput() + if err != nil { + return errors.Wrapf(err, "configure uploadpack.allowFilter: %s", string(output)) + } + return nil } diff --git a/internal/gitclone/manager_test.go b/internal/gitclone/manager_test.go index 439633e..e7e55dd 100644 --- a/internal/gitclone/manager_test.go +++ b/internal/gitclone/manager_test.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "testing" "time" @@ -279,6 +280,46 @@ func TestRepository_Clone_StateVisibleDuringClone(t *testing.T) { assert.Equal(t, StateReady, repo.State()) } +func TestRepository_CloneSetsAllowFilter(t *testing.T) { + ctx := context.Background() + tmpDir := t.TempDir() + + upstreamPath := filepath.Join(tmpDir, "upstream.git") + workPath := filepath.Join(tmpDir, "work") + assert.NoError(t, os.MkdirAll(workPath, 0o755)) + + cmd := exec.Command("git", "-C", workPath, "init") + assert.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", workPath, "config", "user.email", "test@example.com") + assert.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", workPath, "config", "user.name", "Test") + assert.NoError(t, cmd.Run()) + assert.NoError(t, os.WriteFile(filepath.Join(workPath, "f.txt"), []byte("x"), 0o644)) + cmd = exec.Command("git", "-C", workPath, "add", ".") + assert.NoError(t, cmd.Run()) + cmd = exec.Command("git", "-C", workPath, "commit", "-m", "init") + assert.NoError(t, cmd.Run()) + cmd = exec.Command("git", "clone", "--bare", workPath, upstreamPath) + assert.NoError(t, cmd.Run()) + + clonePath := filepath.Join(tmpDir, "clone") + repo := &Repository{ + state: StateEmpty, + path: clonePath, + upstreamURL: upstreamPath, + fetchSem: make(chan struct{}, 1), + } + repo.fetchSem <- struct{}{} + + assert.NoError(t, repo.Clone(ctx)) + assert.Equal(t, StateReady, repo.State()) + + cmd = exec.Command("git", "-C", clonePath, "config", "uploadpack.allowFilter") + output, err := cmd.Output() + assert.NoError(t, err) + assert.Equal(t, "true", strings.TrimSpace(string(output))) +} + func TestRepository_HasCommit(t *testing.T) { ctx := context.Background() tmpDir := t.TempDir() diff --git a/internal/strategy/git/backend.go b/internal/strategy/git/backend.go index 9b0dd8d..7d8f5f6 100644 --- a/internal/strategy/git/backend.go +++ b/internal/strategy/git/backend.go @@ -3,6 +3,7 @@ package git import ( "bytes" "context" + "io" "log/slog" "net/http" "net/http/cgi" //nolint:gosec // CVE-2016-5386 only affects Go < 1.6.3 @@ -67,6 +68,7 @@ func (s *Strategy) serveFromBackend(w http.ResponseWriter, r *http.Request, repo Env: []string{ "GIT_PROJECT_ROOT=" + absRoot, "GIT_HTTP_EXPORT_ALL=1", + "GIT_HTTP_MAX_REQUEST_BUFFER=100M", "PATH=" + os.Getenv("PATH"), }, } @@ -74,6 +76,19 @@ func (s *Strategy) serveFromBackend(w http.ResponseWriter, r *http.Request, repo r2 := r.Clone(r.Context()) r2.URL.Path = backendPath + // Go's cgi.Handler rejects chunked request bodies with a 400. + // Buffer the body so we can set ContentLength and clear TransferEncoding. + if r2.ContentLength < 0 { + body, err := io.ReadAll(r2.Body) + if err != nil { + httputil.ErrorResponse(w, r, http.StatusInternalServerError, "failed to read request body") + return errors.Wrap(err, "read request body") + } + r2.Body = io.NopCloser(bytes.NewReader(body)) + r2.ContentLength = int64(len(body)) + r2.TransferEncoding = nil + } + handler.ServeHTTP(w, r2) if stderrBuf.Len() > 0 {