Skip to content
29 changes: 1 addition & 28 deletions modules/git/batch_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,17 @@
package git

import (
"bufio"
"errors"

"code.gitea.io/gitea/modules/git/catfile"
)

// ReadBatchLine reads the header line from cat-file --batch while preserving the traditional return signature.
func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
sha, typ, size, err = catfile.ReadBatchLine(rd)
return sha, typ, size, convertCatfileError(err, sha)
}

// ReadTagObjectID reads a tag object ID hash from a cat-file --batch stream, throwing away the rest of the stream.
func ReadTagObjectID(rd *bufio.Reader, size int64) (string, error) {
return catfile.ReadTagObjectID(rd, size)
}

// ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream.
func ReadTreeID(rd *bufio.Reader, size int64) (string, error) {
return catfile.ReadTreeID(rd, size)
}

// BinToHex converts a binary hash into a hex encoded one.
func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
return catfile.BinToHex(objectFormat, sha, out)
}

// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream.
func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
func ParseCatFileTreeLine(objectFormat ObjectFormat, rd catfile.ReadCloseDiscarder, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
mode, fname, sha, n, err = catfile.ParseCatFileTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
return mode, fname, sha, n, convertCatfileError(err, nil)
}

// DiscardFull discards the requested number of bytes from the buffered reader.
func DiscardFull(rd *bufio.Reader, discard int64) error {
return catfile.DiscardFull(rd, discard)
}

func convertCatfileError(err error, defaultID []byte) error {
if err == nil {
return nil
Expand Down
64 changes: 19 additions & 45 deletions modules/git/blob_nogogit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
package git

import (
"bufio"
"bytes"
"io"

"code.gitea.io/gitea/modules/git/catfile"
"code.gitea.io/gitea/modules/log"
)

Expand All @@ -26,39 +26,30 @@ type Blob struct {
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync() (io.ReadCloser, error) {
batch, cancel, err := b.repo.CatFileBatch(b.repo.Ctx)
objectInfo, contentReader, err := b.repo.objectPool.Object(b.ID.String())
if err != nil {
if catfile.IsErrObjectNotFound(err) {
return nil, ErrNotExist{ID: b.ID.String()}
}
return nil, err
}

rd := batch.Reader()
_, err = batch.Writer().Write([]byte(b.ID.String() + "\n"))
if err != nil {
cancel()
return nil, err
}
_, _, size, err := ReadBatchLine(rd)
if err != nil {
cancel()
return nil, err
}
b.gotSize = true
b.size = size
b.size = objectInfo.Size

if size < 4096 {
bs, err := io.ReadAll(io.LimitReader(rd, size))
defer cancel()
if b.size < 4096 {
defer contentReader.Close()
bs, err := io.ReadAll(io.LimitReader(contentReader, b.size))
if err != nil {
return nil, err
}
_, err = rd.Discard(1)
_, err = contentReader.Discard(1)
return io.NopCloser(bytes.NewReader(bs)), err
}

return &blobReader{
rd: rd,
n: size,
cancel: cancel,
rd: contentReader,
n: b.size,
}, nil
}

Expand All @@ -68,32 +59,20 @@ func (b *Blob) Size() int64 {
return b.size
}

batch, cancel, err := b.repo.CatFileBatchCheck(b.repo.Ctx)
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
return 0
}
defer cancel()
_, err = batch.Writer().Write([]byte(b.ID.String() + "\n"))
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
return 0
}
_, _, b.size, err = ReadBatchLine(batch.Reader())
objInfo, err := b.repo.objectPool.ObjectInfo(b.ID.String())
if err != nil {
log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err)
return 0
}

b.size = objInfo.Size
b.gotSize = true

return b.size
}

type blobReader struct {
rd *bufio.Reader
n int64
cancel func()
rd catfile.ReadCloseDiscarder
n int64
}

func (b *blobReader) Read(p []byte) (n int, err error) {
Expand All @@ -114,13 +93,8 @@ func (b *blobReader) Close() error {
return nil
}

defer b.cancel()

if err := DiscardFull(b.rd, b.n+1); err != nil {
return err
}

err := catfile.DiscardFull(b.rd, b.n+1)
b.rd.Close()
b.rd = nil

return nil
return err
}
57 changes: 0 additions & 57 deletions modules/git/catfile/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,63 +21,6 @@ type WriteCloserError interface {
CloseWithError(err error) error
}

type Batch interface {
Writer() WriteCloserError
Reader() *bufio.Reader
Close()
}

// batch represents an active `git cat-file --batch` or `--batch-check` invocation
// paired with the pipes that feed/read from it. Call Close to release resources.
type batch struct {
cancel context.CancelFunc
reader *bufio.Reader
writer WriteCloserError
}

// NewBatch creates a new cat-file --batch process for the provided repository path.
// The returned Batch must be closed once the caller has finished with it.
func NewBatch(ctx context.Context, repoPath string) (Batch, error) {
if err := EnsureValidGitRepository(ctx, repoPath); err != nil {
return nil, err
}

var batch batch
batch.writer, batch.reader, batch.cancel = catFileBatch(ctx, repoPath)
return &batch, nil
}

// NewBatchCheck creates a cat-file --batch-check process for the provided repository path.
// The returned Batch must be closed once the caller has finished with it.
func NewBatchCheck(ctx context.Context, repoPath string) (Batch, error) {
if err := EnsureValidGitRepository(ctx, repoPath); err != nil {
return nil, err
}

var check batch
check.writer, check.reader, check.cancel = catFileBatchCheck(ctx, repoPath)
return &check, nil
}

func (b *batch) Writer() WriteCloserError {
return b.writer
}

func (b *batch) Reader() *bufio.Reader {
return b.reader
}

// Close stops the underlying git cat-file process and releases held resources.
func (b *batch) Close() {
if b == nil || b.cancel == nil {
return
}
b.cancel()
b.reader = nil
b.writer = nil
b.cancel = nil
}

// EnsureValidGitRepository runs `git rev-parse` in the repository path to make sure
// the directory is a valid git repository. This avoids git cat-file hanging indefinitely
// when invoked in invalid paths.
Expand Down
31 changes: 31 additions & 0 deletions modules/git/catfile/object_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package catfile

import (
"io"
)

type ObjectInfo struct {
ID string
Type string
Size int64
}

type Discarder interface {
Discard(n int) (int, error)
}

type ReadCloseDiscarder interface {
io.ReadCloser
Discarder
ReadBytes(delim byte) ([]byte, error)
ReadSlice(delim byte) (line []byte, err error)
}

type ObjectPool interface {
ObjectInfo(refName string) (*ObjectInfo, error)
Object(refName string) (*ObjectInfo, ReadCloseDiscarder, error)
Close()
}
Loading