From 3580591e28d378e0453b53edc6a1afe794138728 Mon Sep 17 00:00:00 2001 From: somaz <112675579+somaz94@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:33:15 +0900 Subject: [PATCH] fix(repo): avoid a repo-sized temp file for every bundle download (#38863) Bundle downloads run `git bundle create` into a temp file under `data/tmp/git-repo-content` and copy it to the response, so every download puts a second, repo-sized copy on disk before the first byte is sent. If the process dies mid-request that copy is stranded: the startup sweep only drops files older than 3 days, and cannot remove a directory that still holds a newer file. Streaming `git bundle create -` to the response removes that copy. `CreateArchive` above already uses the same gitcmd pattern, and the bundle bytes are unchanged (existing integration assertions on the exact length still pass). One consequence: git can now fail after output starts, so a mid-stream failure truncates the body instead of returning an error. I did not act on the TODO. A temp ref only works under `refs/heads/*`; with `refs/bundle/temp-*` the bundle carries no branch and clones empty, so I noted that on the TODO. Fixes https://github.com/go-gitea/gitea/issues/38447 --------- Co-authored-by: silverwind --- modules/git/archive.go | 24 ++++++------------------ modules/git/archive_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 modules/git/archive_test.go diff --git a/modules/git/archive.go b/modules/git/archive.go index 159db42011..4259b8db1f 100644 --- a/modules/git/archive.go +++ b/modules/git/archive.go @@ -39,10 +39,11 @@ func CreateArchive(ctx context.Context, repo RepositoryFacade, repoName, format // CreateBundle create bundle content to the target path func CreateBundle(ctx context.Context, repo RepositoryFacade, commit string, out io.Writer) error { - // TODO: use the following steps instead of creating a temp file, also need to iterate and clean up outdated refs - // git update-ref refs/bundle/temp-{timestamp} {commit} - // git bundle create - refs/bundle/temp-{timestamp} - // git update-ref -d refs/bundle/temp-{timestamp} + // TODO: use the following steps instead of creating a temp repo, also need to iterate and clean up outdated refs + // the temp ref has to be under refs/heads/*, and a clone only checks out with a HEAD, which needs the temp repo + // git update-ref refs/heads/bundle-temp-{timestamp} {commit} + // git bundle create - refs/heads/bundle-temp-{timestamp} + // git update-ref -d refs/heads/bundle-temp-{timestamp} tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle") if err != nil { return err @@ -69,18 +70,5 @@ func CreateBundle(ctx context.Context, repo RepositoryFacade, commit string, out return err } - tmpFile := filepath.Join(tmpDir, "bundle") - _, _, err = gitTmpCmd().AddArguments("bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(ctx) - if err != nil { - return err - } - - fi, err := os.Open(tmpFile) - if err != nil { - return err - } - defer fi.Close() - - _, err = io.Copy(out, fi) - return err + return gitTmpCmd().AddArguments("bundle", "create", "-", "bundle", "HEAD").WithStdoutCopy(out).RunWithStderr(ctx) } diff --git a/modules/git/archive_test.go b/modules/git/archive_test.go new file mode 100644 index 0000000000..18a05dc48d --- /dev/null +++ b/modules/git/archive_test.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bytes" + "strings" + "testing" + + "gitea.dev/modules/setting" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateBundle(t *testing.T) { + setting.AppDataPath = t.TempDir() + + buf := &bytes.Buffer{} + require.NoError(t, CreateBundle(t.Context(), mockRepository("repo1_bare"), "ce064814f4a0d337b333e646ece456cd39fab612", buf)) + + header, _, ok := strings.Cut(buf.String(), "\n\n") + require.True(t, ok) + assert.Equal(t, "# v2 git bundle", strings.Split(header, "\n")[0]) + // without a refs/heads/* ref and a HEAD, a clone of the bundle has no branch and no checkout + assert.Contains(t, header, "ce064814f4a0d337b333e646ece456cd39fab612 refs/heads/bundle") + assert.Contains(t, header, "ce064814f4a0d337b333e646ece456cd39fab612 HEAD") +}