drone/git/api/cat-file.go

160 lines
4.2 KiB
Go

// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"bufio"
"bytes"
"context"
"io"
"strconv"
"strings"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/command"
"github.com/harness/gitness/git/sha"
"github.com/djherbis/buffer"
"github.com/djherbis/nio/v3"
)
// WriteCloserError wraps an io.WriteCloser with an additional CloseWithError function.
type WriteCloserError interface {
io.WriteCloser
CloseWithError(err error) error
}
// CatFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe,
// a stdout reader and cancel function.
func CatFileBatch(
ctx context.Context,
repoPath string,
alternateObjectDirs []string,
flags ...command.CmdOptionFunc,
) (WriteCloserError, *bufio.Reader, func()) {
flags = append(flags, command.WithFlag("--batch"))
return catFileBatch(ctx, repoPath, alternateObjectDirs, flags...)
}
func CatFileBatchCheck(
ctx context.Context,
repoPath string,
alternateObjectDirs []string,
flags ...command.CmdOptionFunc,
) (WriteCloserError, *bufio.Reader, func()) {
flags = append(flags, command.WithFlag("--batch-check"))
return catFileBatch(ctx, repoPath, alternateObjectDirs, flags...)
}
func catFileBatch(
ctx context.Context,
repoPath string,
alternateObjectDirs []string,
flags ...command.CmdOptionFunc,
) (WriteCloserError, *bufio.Reader, func()) {
const bufferSize = 32 * 1024
// We often want to feed the commits in order into cat-file --batch,
// followed by their trees and sub trees as necessary.
batchStdinReader, batchStdinWriter := io.Pipe()
batchStdoutReader, batchStdoutWriter := nio.Pipe(buffer.New(bufferSize))
ctx, ctxCancel := context.WithCancel(ctx)
closed := make(chan struct{})
cancel := func() {
ctxCancel()
_ = batchStdinWriter.Close()
_ = batchStdoutReader.Close()
<-closed
}
// Ensure cancel is called as soon as the provided context is cancelled
go func() {
<-ctx.Done()
cancel()
}()
go func() {
stderr := bytes.Buffer{}
cmd := command.New("cat-file",
command.WithAlternateObjectDirs(alternateObjectDirs...),
)
cmd.Add(flags...)
err := cmd.Run(ctx,
command.WithDir(repoPath),
command.WithStdin(batchStdinReader),
command.WithStdout(batchStdoutWriter),
command.WithStderr(&stderr),
)
if err != nil {
_ = batchStdoutWriter.CloseWithError(command.NewError(err, stderr.Bytes()))
_ = batchStdinReader.CloseWithError(command.NewError(err, stderr.Bytes()))
} else {
_ = batchStdoutWriter.Close()
_ = batchStdinReader.Close()
}
close(closed)
}()
// For simplicities sake we'll us a buffered reader to read from the cat-file --batch
batchReader := bufio.NewReaderSize(batchStdoutReader, bufferSize)
return batchStdinWriter, batchReader, cancel
}
type BatchHeaderResponse struct {
SHA sha.SHA
Type string
Size int64
}
// ReadBatchHeaderLine reads the header line from cat-file --batch
// <sha> SP <type> SP <size> LF
// sha is a 40byte not 20byte here.
func ReadBatchHeaderLine(rd *bufio.Reader) (*BatchHeaderResponse, error) {
line, err := rd.ReadString('\n')
if err != nil {
return nil, err
}
if len(line) == 1 {
line, err = rd.ReadString('\n')
if err != nil {
return nil, err
}
}
idx := strings.IndexByte(line, ' ')
if idx < 0 {
return nil, errors.NotFound("missing space char for: %s", line)
}
id := line[:idx]
objType := line[idx+1:]
idx = strings.IndexByte(objType, ' ')
if idx < 0 {
return nil, errors.NotFound("sha '%s' not found", id)
}
sizeStr := objType[idx+1 : len(objType)-1]
objType = objType[:idx]
size, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
return nil, err
}
return &BatchHeaderResponse{
SHA: sha.Must(id),
Type: objType,
Size: size,
}, nil
}