Refactor git command stderr handling (#36402)

And clean up legacy fragile & incorrect logic
This commit is contained in:
wxiaoguang
2026-01-19 07:10:33 +08:00
committed by GitHub
parent fafd1db19e
commit 72be55f7d3
69 changed files with 345 additions and 627 deletions

View File

@@ -76,13 +76,11 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string)
_ = stdinReader.Close()
_ = lw.Close()
}()
stdErr := new(bytes.Buffer)
err := cmd.WithEnv(envs).
WithDir(repo.Path).
WithStdin(stdinReader).
WithStdout(lw).
WithStderr(stdErr).
Run(ctx)
RunWithStderr(ctx)
if err != nil && !git.IsErrCanceledOrKilled(err) {
log.Error("Attribute checker for commit %s exits with error: %v", treeish, err)

View File

@@ -69,14 +69,11 @@ func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish strin
defer cancel()
stdOut := new(bytes.Buffer)
stdErr := new(bytes.Buffer)
if err := cmd.WithEnv(append(os.Environ(), envs...)).
WithDir(gitRepo.Path).
WithStdout(stdOut).
WithStderr(stdErr).
Run(ctx); err != nil {
return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String())
RunWithStderr(ctx); err != nil {
return nil, fmt.Errorf("failed to run check-attr: %w", err)
}
fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})

View File

@@ -7,6 +7,7 @@ import (
"bufio"
"bytes"
"context"
"errors"
"io"
"math"
"strconv"
@@ -40,15 +41,13 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co
var batchStdinWriter io.WriteCloser
var batchStdoutReader io.ReadCloser
stderr := strings.Builder{}
cmdCatFile = cmdCatFile.
WithDir(repoPath).
WithStdinWriter(&batchStdinWriter).
WithStdoutReader(&batchStdoutReader).
WithStderr(&stderr).
WithUseContextTimeout(true)
err := cmdCatFile.Start(ctx)
err := cmdCatFile.StartWithStderr(ctx)
if err != nil {
log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err)
// ideally here it should return the error, but it would require refactoring all callers
@@ -63,9 +62,9 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co
}
go func() {
err := cmdCatFile.Wait()
if err != nil {
log.Error("cat-file --batch command failed in repo %s: %v - stderr: %s", repoPath, err, stderr.String())
err := cmdCatFile.WaitWithStderr()
if err != nil && !errors.Is(err, context.Canceled) {
log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err)
}
ctxCancel(err)
}()

View File

@@ -120,7 +120,7 @@ func CommitChanges(ctx context.Context, repoPath string, opts CommitChangesOptio
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
// No stderr but exit status 1 means nothing to commit.
if err != nil && err.Error() == "exit status 1" {
if gitcmd.IsErrorExitCode(err, 1) {
return nil
}
return err
@@ -315,7 +315,7 @@ func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, err
WithDir(repoPath).
RunStdString(ctx)
if err != nil {
if strings.Contains(err.Error(), "exit status 128") {
if gitcmd.IsErrorExitCode(err, 128) {
return "", ErrNotExist{shortID, ""}
}
return "", err

View File

@@ -5,7 +5,6 @@ package git
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
@@ -80,14 +79,9 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
return fmt.Errorf("invalid diffType: %s", diffType)
}
stderr := new(bytes.Buffer)
if err = cmd.WithDir(repo.Path).
return cmd.WithDir(repo.Path).
WithStdout(writer).
WithStderr(stderr).
Run(repo.Ctx); err != nil {
return fmt.Errorf("Run: %w - %s", err, stderr)
}
return nil
RunWithStderr(repo.Ctx)
}
// ParseDiffHunkString parse the diff hunk content and return

View File

@@ -40,6 +40,7 @@ const DefaultLocale = "C"
// Command represents a command with its subcommands or arguments.
type Command struct {
callerInfo string
prog string
args []string
preErrors []error
@@ -52,9 +53,10 @@ type Command struct {
cmdFinished context.CancelFunc
cmdStartTime time.Time
cmdStdinWriter *io.WriteCloser
cmdStdoutReader *io.ReadCloser
cmdStderrReader *io.ReadCloser
cmdStdinWriter *io.WriteCloser
cmdStdoutReader *io.ReadCloser
cmdStderrReader *io.ReadCloser
cmdManagedStderr *bytes.Buffer
}
func logArgSanitize(arg string) string {
@@ -221,7 +223,7 @@ type runOpts struct {
// The correct approach is to use `--git-dir" global argument
Dir string
Stdout, Stderr io.Writer
Stdout io.Writer
// Stdin is used for passing input to the command
// The caller must make sure the Stdin writer is closed properly to finish the Run function.
@@ -235,8 +237,6 @@ type runOpts struct {
Stdin io.Reader
PipelineFunc func(context.Context, context.CancelFunc) error
callerInfo string
}
func commonBaseEnvs() []string {
@@ -310,12 +310,6 @@ func (c *Command) WithStderrReader(r *io.ReadCloser) *Command {
return c
}
// WithStderr is deprecated, use WithStderrReader instead
func (c *Command) WithStderr(stderr io.Writer) *Command {
c.opts.Stderr = stderr
return c
}
func (c *Command) WithStdinWriter(w *io.WriteCloser) *Command {
c.cmdStdinWriter = w
return c
@@ -343,11 +337,11 @@ func (c *Command) WithUseContextTimeout(useContextTimeout bool) *Command {
// then you can to call this function in GeneralWrapperFunc to set the caller info of FeatureFunc.
// The caller info can only be set once.
func (c *Command) WithParentCallerInfo(optInfo ...string) *Command {
if c.opts.callerInfo != "" {
if c.callerInfo != "" {
return c
}
if len(optInfo) > 0 {
c.opts.callerInfo = optInfo[0]
c.callerInfo = optInfo[0]
return c
}
skip := 1 /*parent "wrap/run" functions*/ + 1 /*this function*/
@@ -356,7 +350,7 @@ func (c *Command) WithParentCallerInfo(optInfo ...string) *Command {
if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 {
callerInfo = callerInfo[pos+1:]
}
c.opts.callerInfo = callerInfo
c.callerInfo = callerInfo
return c
}
@@ -372,7 +366,7 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
safeClosePtrCloser(c.cmdStdoutReader)
safeClosePtrCloser(c.cmdStderrReader)
safeClosePtrCloser(c.cmdStdinWriter)
// if no error, cmdFinished will be called in "Wait" function
// if error occurs, we must also finish the task, otherwise, cmdFinished will be called in "Wait" function
if c.cmdFinished != nil {
c.cmdFinished()
}
@@ -393,16 +387,16 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
}
cmdLogString := c.LogString()
if c.opts.callerInfo == "" {
if c.callerInfo == "" {
c.WithParentCallerInfo()
}
// these logs are for debugging purposes only, so no guarantee of correctness or stability
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.opts.callerInfo, logArgSanitize(c.opts.Dir), cmdLogString)
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.opts.Dir), cmdLogString)
log.Debug("git.Command: %s", desc)
_, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun)
defer span.End()
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.opts.callerInfo)
span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo)
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)
if c.opts.UseContextTimeout {
@@ -425,7 +419,6 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
cmd.Env = append(cmd.Env, CommonGitCmdEnvs()...)
cmd.Dir = c.opts.Dir
cmd.Stdout = c.opts.Stdout
cmd.Stderr = c.opts.Stderr
cmd.Stdin = c.opts.Stdin
if _, err := safeAssignPipe(c.cmdStdinWriter, cmd.StdinPipe); err != nil {
@@ -437,19 +430,32 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
if _, err := safeAssignPipe(c.cmdStderrReader, cmd.StderrPipe); err != nil {
return err
}
if c.cmdManagedStderr != nil {
if cmd.Stderr != nil {
panic("CombineStderr needs managed (but not caller-provided) stderr pipe")
}
cmd.Stderr = c.cmdManagedStderr
}
return cmd.Start()
}
func (c *Command) Wait() error {
defer c.cmdFinished()
defer func() {
safeClosePtrCloser(c.cmdStdoutReader)
safeClosePtrCloser(c.cmdStderrReader)
safeClosePtrCloser(c.cmdStdinWriter)
c.cmdFinished()
}()
cmd, ctx, cancel := c.cmd, c.cmdCtx, c.cmdCancel
if c.opts.PipelineFunc != nil {
err := c.opts.PipelineFunc(ctx, cancel)
if err != nil {
cancel()
_ = cmd.Wait()
return err
errWait := cmd.Wait()
return errors.Join(err, errWait)
}
}
@@ -472,6 +478,34 @@ func (c *Command) Wait() error {
return errCause
}
func (c *Command) StartWithStderr(ctx context.Context) RunStdError {
c.cmdManagedStderr = &bytes.Buffer{}
err := c.Start(ctx)
if err != nil {
return &runStdError{err: err}
}
return nil
}
func (c *Command) WaitWithStderr() RunStdError {
if c.cmdManagedStderr == nil {
panic("CombineStderr needs managed (but not caller-provided) stderr pipe")
}
errWait := c.Wait()
if errWait == nil {
// if no exec error but only stderr output, the stderr output is still saved in "c.cmdManagedStderr" and can be read later
return nil
}
return &runStdError{err: errWait, stderr: util.UnsafeBytesToString(c.cmdManagedStderr.Bytes())}
}
func (c *Command) RunWithStderr(ctx context.Context) RunStdError {
if err := c.StartWithStderr(ctx); err != nil {
return &runStdError{err: err}
}
return c.WaitWithStderr()
}
func (c *Command) Run(ctx context.Context) (err error) {
if err = c.Start(ctx); err != nil {
return err
@@ -493,9 +527,9 @@ type runStdError struct {
func (r *runStdError) Error() string {
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
// But a lof of code only checks `strings.Contains(err.Error(), "git error")`
// But a lot of code only checks `strings.Contains(err.Error(), "git error")`
if r.errMsg == "" {
r.errMsg = ConcatenateError(r.err, r.stderr).Error()
r.errMsg = fmt.Sprintf("%s - %s", r.err.Error(), strings.TrimSpace(r.stderr))
}
return r.errMsg
}
@@ -543,24 +577,16 @@ func (c *Command) RunStdBytes(ctx context.Context) (stdout, stderr []byte, runEr
return c.WithParentCallerInfo().runStdBytes(ctx)
}
func (c *Command) runStdBytes(ctx context.Context) ( /*stdout*/ []byte /*stderr*/, []byte /*runErr*/, RunStdError) {
if c.opts.Stdout != nil || c.opts.Stderr != nil {
func (c *Command) runStdBytes(ctx context.Context) ([]byte, []byte, RunStdError) {
if c.opts.Stdout != nil || c.cmdStdoutReader != nil || c.cmdStderrReader != nil {
// we must panic here, otherwise there would be bugs if developers set Stdin/Stderr by mistake, and it would be very difficult to debug
panic("stdout and stderr field must be nil when using RunStdBytes")
}
stdoutBuf := &bytes.Buffer{}
stderrBuf := &bytes.Buffer{}
err := c.WithParentCallerInfo().
WithStdout(stdoutBuf).
WithStderr(stderrBuf).
Run(ctx)
if err != nil {
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
// But a lot of code depends on it, so we have to keep this behavior
return nil, stderrBuf.Bytes(), &runStdError{err: err, stderr: util.UnsafeBytesToString(stderrBuf.Bytes())}
}
// even if there is no err, there could still be some stderr output
return stdoutBuf.Bytes(), stderrBuf.Bytes(), nil
RunWithStderr(ctx)
return stdoutBuf.Bytes(), c.cmdManagedStderr.Bytes(), err
}
func (c *Command) DebugKill() {

View File

@@ -45,7 +45,7 @@ func TestRunWithContextStd(t *testing.T) {
assert.Equal(t, stderr, err.Stderr())
assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr())
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such\n", err.Error())
assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such", err.Error())
assert.Empty(t, stdout)
}
}
@@ -57,7 +57,7 @@ func TestRunWithContextStd(t *testing.T) {
assert.Equal(t, string(stderr), err.Stderr())
assert.Equal(t, "fatal: Not a valid object name no-such\n", err.Stderr())
// FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such\n", err.Error())
assert.Equal(t, "exit status 128 - fatal: Not a valid object name no-such", err.Error())
assert.Empty(t, stdout)
}
}

View File

@@ -4,22 +4,9 @@
package gitcmd
import (
"fmt"
"io"
"code.gitea.io/gitea/modules/util"
)
// ConcatenateError concatenates an error with stderr string
// FIXME: use RunStdError instead
func ConcatenateError(err error, stderr string) error {
if len(stderr) == 0 {
return err
}
errMsg := fmt.Sprintf("%s - %s", err.Error(), stderr)
return util.ErrorWrap(&runStdError{err: err, stderr: stderr, errMsg: errMsg}, "%s", errMsg)
}
func safeClosePtrCloser[T *io.ReadCloser | *io.WriteCloser](c T) {
switch v := any(c).(type) {
case *io.ReadCloser:

View File

@@ -5,11 +5,10 @@ package git
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
"io"
"slices"
"strconv"
"strings"
@@ -42,15 +41,6 @@ type GrepOptions struct {
}
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("unable to create os pipe to grep: %w", err)
}
defer func() {
_ = stdoutReader.Close()
_ = stdoutWriter.Close()
}()
/*
The output is like this ( "^@" means \x00):
@@ -83,12 +73,11 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
cmd.AddDashesAndList(opts.PathspecList...)
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
stderr := bytes.Buffer{}
err = cmd.WithDir(repo.Path).
WithStdout(stdoutWriter).
WithStderr(&stderr).
var stdoutReader io.ReadCloser
err := cmd.WithDir(repo.Path).
WithStdoutReader(&stdoutReader).
WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error {
_ = stdoutWriter.Close()
defer stdoutReader.Close()
isInBlock := false
@@ -133,17 +122,17 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
}
return nil
}).
Run(ctx)
RunWithStderr(ctx)
// git grep exits by cancel (killed), usually it is caused by the limit of results
if gitcmd.IsErrorExitCode(err, -1) && stderr.Len() == 0 {
if gitcmd.IsErrorExitCode(err, -1) && err.Stderr() == "" {
return results, nil
}
// git grep exits with 1 if no results are found
if gitcmd.IsErrorExitCode(err, 1) && stderr.Len() == 0 {
if gitcmd.IsErrorExitCode(err, 1) && err.Stderr() == "" {
return nil, nil
}
if err != nil && !errors.Is(err, context.Canceled) {
return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String())
return nil, fmt.Errorf("unable to run git grep: %w", err)
}
return results, nil
}

View File

@@ -64,20 +64,12 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p
cmd.AddDashesAndList(files...)
go func() {
stderr := strings.Builder{}
err := cmd.WithDir(repository).
WithStdout(stdoutWriter).
WithStderr(&stderr).
Run(ctx)
if err != nil {
_ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
return
}
_ = stdoutWriter.Close()
RunWithStderr(ctx)
_ = stdoutWriter.CloseWithError(err)
}()
// For simplicities sake we'll us a buffered reader to read from the cat-file --batch
bufReader := bufio.NewReaderSize(stdoutReader, 32*1024)
return bufReader, cancel

View File

@@ -5,7 +5,6 @@ package pipeline
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
@@ -14,7 +13,6 @@ import (
"sync"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
)
// CatFileBatchCheck runs cat-file with --batch-check
@@ -23,15 +21,12 @@ func CatFileBatchCheck(ctx context.Context, shasToCheckReader *io.PipeReader, ca
defer shasToCheckReader.Close()
defer catFileCheckWriter.Close()
stderr := new(bytes.Buffer)
var errbuf strings.Builder
cmd := gitcmd.NewCommand("cat-file", "--batch-check")
if err := cmd.WithDir(tmpBasePath).
WithStdin(shasToCheckReader).
WithStdout(catFileCheckWriter).
WithStderr(stderr).
Run(ctx); err != nil {
_ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check [%s]: %w - %s", tmpBasePath, err, errbuf.String()))
RunWithStderr(ctx); err != nil {
_ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check [%s]: %w", tmpBasePath, err))
}
}
@@ -40,16 +35,11 @@ func CatFileBatchCheckAllObjects(ctx context.Context, catFileCheckWriter *io.Pip
defer wg.Done()
defer catFileCheckWriter.Close()
stderr := new(bytes.Buffer)
var errbuf strings.Builder
cmd := gitcmd.NewCommand("cat-file", "--batch-check", "--batch-all-objects")
if err := cmd.WithDir(tmpBasePath).
WithStdout(catFileCheckWriter).
WithStderr(stderr).
Run(ctx); err != nil {
log.Error("git cat-file --batch-check --batch-all-object [%s]: %v - %s", tmpBasePath, err, errbuf.String())
err = fmt.Errorf("git cat-file --batch-check --batch-all-object [%s]: %w - %s", tmpBasePath, err, errbuf.String())
_ = catFileCheckWriter.CloseWithError(err)
RunWithStderr(ctx); err != nil {
_ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check --batch-all-object [%s]: %w", tmpBasePath, err))
errChan <- err
}
}
@@ -60,15 +50,12 @@ func CatFileBatch(ctx context.Context, shasToBatchReader *io.PipeReader, catFile
defer shasToBatchReader.Close()
defer catFileBatchWriter.Close()
stderr := new(bytes.Buffer)
var errbuf strings.Builder
if err := gitcmd.NewCommand("cat-file", "--batch").
WithDir(tmpBasePath).
WithStdin(shasToBatchReader).
WithStdout(catFileBatchWriter).
WithStderr(stderr).
Run(ctx); err != nil {
_ = shasToBatchReader.CloseWithError(fmt.Errorf("git rev-list [%s]: %w - %s", tmpBasePath, err, errbuf.String()))
RunWithStderr(ctx); err != nil {
_ = shasToBatchReader.CloseWithError(fmt.Errorf("git rev-list [%s]: %w", tmpBasePath, err))
}
}

View File

@@ -33,17 +33,11 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
}()
go func() {
stderr := strings.Builder{}
err := gitcmd.NewCommand("rev-list", "--all").
WithDir(repo.Path).
WithStdout(revListWriter).
WithStderr(&stderr).
Run(repo.Ctx)
if err != nil {
_ = revListWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
} else {
_ = revListWriter.Close()
}
RunWithStderr(repo.Ctx)
_ = revListWriter.CloseWithError(err)
}()
// Next feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary.

View File

@@ -4,11 +4,9 @@
package pipeline
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"sync"
"code.gitea.io/gitea/modules/git/gitcmd"
@@ -20,14 +18,11 @@ func NameRevStdin(ctx context.Context, shasToNameReader *io.PipeReader, nameRevS
defer shasToNameReader.Close()
defer nameRevStdinWriter.Close()
stderr := new(bytes.Buffer)
var errbuf strings.Builder
if err := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").
WithDir(tmpBasePath).
WithStdin(shasToNameReader).
WithStdout(nameRevStdinWriter).
WithStderr(stderr).
Run(ctx); err != nil {
_ = shasToNameReader.CloseWithError(fmt.Errorf("git name-rev [%s]: %w - %s", tmpBasePath, err, errbuf.String()))
RunWithStderr(ctx); err != nil {
_ = shasToNameReader.CloseWithError(fmt.Errorf("git name-rev [%s]: %w", tmpBasePath, err))
}
}

View File

@@ -5,7 +5,6 @@ package pipeline
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
@@ -13,7 +12,6 @@ import (
"sync"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
)
// RevListAllObjects runs rev-list --objects --all and writes to a pipewriter
@@ -21,16 +19,11 @@ func RevListAllObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sy
defer wg.Done()
defer revListWriter.Close()
stderr := new(bytes.Buffer)
var errbuf strings.Builder
cmd := gitcmd.NewCommand("rev-list", "--objects", "--all")
if err := cmd.WithDir(basePath).
WithStdout(revListWriter).
WithStderr(stderr).
Run(ctx); err != nil {
log.Error("git rev-list --objects --all [%s]: %v - %s", basePath, err, errbuf.String())
err = fmt.Errorf("git rev-list --objects --all [%s]: %w - %s", basePath, err, errbuf.String())
_ = revListWriter.CloseWithError(err)
RunWithStderr(ctx); err != nil {
_ = revListWriter.CloseWithError(fmt.Errorf("git rev-list --objects --all [%s]: %w", basePath, err))
errChan <- err
}
}
@@ -39,18 +32,15 @@ func RevListAllObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sy
func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath, headSHA, baseSHA string, errChan chan<- error) {
defer wg.Done()
defer revListWriter.Close()
stderr := new(bytes.Buffer)
var errbuf strings.Builder
cmd := gitcmd.NewCommand("rev-list", "--objects").AddDynamicArguments(headSHA)
if baseSHA != "" {
cmd = cmd.AddArguments("--not").AddDynamicArguments(baseSHA)
}
if err := cmd.WithDir(tmpBasePath).
WithStdout(revListWriter).
WithStderr(stderr).
Run(ctx); err != nil {
log.Error("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String())
errChan <- fmt.Errorf("git rev-list [%s]: %w - %s", tmpBasePath, err, errbuf.String())
RunWithStderr(ctx); err != nil {
errChan <- fmt.Errorf("git rev-list [%s]: %w", tmpBasePath, err)
}
}

View File

@@ -8,7 +8,6 @@ import (
"bytes"
"context"
"fmt"
"io"
"net/url"
"os"
"path"
@@ -83,22 +82,19 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma
// IsEmpty Check if repository is empty.
func (repo *Repository) IsEmpty() (bool, error) {
var errbuf, output strings.Builder
if err := gitcmd.NewCommand().
stdout, _, err := gitcmd.NewCommand().
AddOptionFormat("--git-dir=%s", repo.Path).
AddArguments("rev-list", "-n", "1", "--all").
WithDir(repo.Path).
WithStdout(&output).
WithStderr(&errbuf).
Run(repo.Ctx); err != nil {
if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" {
RunStdString(repo.Ctx)
if err != nil {
if (gitcmd.IsErrorExitCode(err, 1) && err.Stderr() == "") || gitcmd.IsErrorExitCode(err, 129) {
// git 2.11 exits with 129 if the repo is empty
return true, nil
}
return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String())
return true, fmt.Errorf("check empty: %w", err)
}
return strings.TrimSpace(output.String()) == "", nil
return strings.TrimSpace(stdout) == "", nil
}
// CloneRepoOptions options when clone a repository
@@ -171,16 +167,10 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
}
}
stderr := new(bytes.Buffer)
if err := cmd.
return cmd.
WithTimeout(opts.Timeout).
WithEnv(envs).
WithStdout(io.Discard).
WithStderr(stderr).
Run(ctx); err != nil {
return gitcmd.ConcatenateError(err, stderr.String())
}
return nil
RunWithStderr(ctx)
}
// PushOptions options when push to remote

View File

@@ -62,13 +62,7 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t
cmd.AddOptionFormat("--format=%s", format.String())
cmd.AddDynamicArguments(commitID)
var stderr strings.Builder
err := cmd.WithDir(repo.Path).
return cmd.WithDir(repo.Path).
WithStdout(target).
WithStderr(&stderr).
Run(ctx)
if err != nil {
return gitcmd.ConcatenateError(err, stderr.String())
}
return nil
RunWithStderr(ctx)
}

View File

@@ -101,23 +101,13 @@ func WalkShowRef(ctx context.Context, repoPath string, extraArgs gitcmd.TrustedC
}()
go func() {
stderrBuilder := &strings.Builder{}
args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"}
args = append(args, extraArgs...)
err := gitcmd.NewCommand(args...).
WithDir(repoPath).
WithStdout(stdoutWriter).
WithStderr(stderrBuilder).
Run(ctx)
if err != nil {
if stderrBuilder.Len() == 0 {
_ = stdoutWriter.Close()
return
}
_ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, stderrBuilder.String()))
} else {
_ = stdoutWriter.Close()
}
RunWithStderr(ctx)
_ = stdoutWriter.CloseWithError(err)
}()
i := 0

View File

@@ -232,7 +232,6 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
_ = stdoutWriter.Close()
}()
go func() {
stderr := strings.Builder{}
gitCmd := gitcmd.NewCommand("rev-list").
AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
@@ -251,13 +250,8 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
gitCmd.AddDashesAndList(opts.File)
err := gitCmd.WithDir(repo.Path).
WithStdout(stdoutWriter).
WithStderr(&stderr).
Run(repo.Ctx)
if err != nil {
_ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String()))
} else {
_ = stdoutWriter.Close()
}
RunWithStderr(repo.Ctx)
_ = stdoutWriter.CloseWithError(err)
}()
objectFormat, err := repo.GetObjectFormat()

View File

@@ -34,7 +34,6 @@ func (l *lineCountWriter) Write(p []byte) (n int, err error) {
func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparison bool) (int, error) {
// Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly
w := &lineCountWriter{}
stderr := new(bytes.Buffer)
separator := "..."
if directComparison {
@@ -47,24 +46,21 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis
AddArguments("--").
WithDir(repo.Path).
WithStdout(w).
WithStderr(stderr).
Run(repo.Ctx); err != nil {
if strings.Contains(stderr.String(), "no merge base") {
RunWithStderr(repo.Ctx); err != nil {
if strings.Contains(err.Stderr(), "no merge base") {
// git >= 2.28 now returns an error if base and head have become unrelated.
// previously it would return the results of git diff -z --name-only base head so let's try that...
w = &lineCountWriter{}
stderr.Reset()
if err = gitcmd.NewCommand("diff", "-z", "--name-only").
AddDynamicArguments(base, head).
AddArguments("--").
WithDir(repo.Path).
WithStdout(w).
WithStderr(stderr).
Run(repo.Ctx); err == nil {
RunWithStderr(repo.Ctx); err == nil {
return w.numLines, nil
}
}
return 0, fmt.Errorf("%w: Stderr: %s", err, stderr)
return 0, err
}
return w.numLines, nil
}
@@ -73,11 +69,9 @@ var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`)
// GetDiff generates and returns patch data between given revisions, optimized for human readability
func (repo *Repository) GetDiff(compareArg string, w io.Writer) error {
stderr := new(bytes.Buffer)
return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg).
WithDir(repo.Path).
WithStdout(w).
WithStderr(stderr).
Run(repo.Ctx)
}
@@ -92,11 +86,9 @@ func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error {
// GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply`
func (repo *Repository) GetPatch(compareArg string, w io.Writer) error {
stderr := new(bytes.Buffer)
return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg).
WithDir(repo.Path).
WithStdout(w).
WithStderr(stderr).
Run(repo.Ctx)
}

View File

@@ -101,21 +101,17 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
return err
}
cmd := gitcmd.NewCommand("update-index", "--remove", "-z", "--index-info")
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
buffer := new(bytes.Buffer)
input := new(bytes.Buffer)
for _, file := range filenames {
if file != "" {
// using format: mode SP type SP sha1 TAB path
buffer.WriteString("0 blob " + objectFormat.EmptyObjectID().String() + "\t" + file + "\000")
input.WriteString("0 blob " + objectFormat.EmptyObjectID().String() + "\t" + file + "\000")
}
}
return cmd.
WithDir(repo.Path).
WithStdin(bytes.NewReader(buffer.Bytes())).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
WithStdin(bytes.NewReader(input.Bytes())).
RunWithStderr(repo.Ctx)
}
type IndexObjectInfo struct {
@@ -127,19 +123,15 @@ type IndexObjectInfo struct {
// AddObjectsToIndex adds the provided object hashes to the index at the provided filenames
func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error {
cmd := gitcmd.NewCommand("update-index", "--add", "--replace", "-z", "--index-info")
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
buffer := new(bytes.Buffer)
input := new(bytes.Buffer)
for _, object := range objects {
// using format: mode SP type SP sha1 TAB path
buffer.WriteString(object.Mode + " blob " + object.Object.String() + "\t" + object.Filename + "\000")
input.WriteString(object.Mode + " blob " + object.Object.String() + "\t" + object.Filename + "\000")
}
return cmd.
WithDir(repo.Path).
WithStdin(bytes.NewReader(buffer.Bytes())).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
WithStdin(bytes.NewReader(input.Bytes())).
RunWithStderr(repo.Ctx)
}
// AddObjectToIndex adds the provided object hash to the index at the provided filename

View File

@@ -5,7 +5,6 @@
package git
import (
"bytes"
"io"
"strings"
@@ -74,16 +73,12 @@ func (repo *Repository) hashObject(reader io.Reader, save bool) (string, error)
} else {
cmd = gitcmd.NewCommand("hash-object", "--stdin")
}
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
err := cmd.
stdout, _, err := cmd.
WithDir(repo.Path).
WithStdin(reader).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
RunStdString(repo.Ctx)
if err != nil {
return "", err
}
return strings.TrimSpace(stdout.String()), nil
return strings.TrimSpace(stdout), nil
}

View File

@@ -22,17 +22,11 @@ func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
}()
go func() {
stderrBuilder := &strings.Builder{}
err := gitcmd.NewCommand("for-each-ref").
WithDir(repo.Path).
WithStdout(stdoutWriter).
WithStderr(stderrBuilder).
Run(repo.Ctx)
if err != nil {
_ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, stderrBuilder.String()))
} else {
_ = stdoutWriter.Close()
}
_ = stdoutWriter.CloseWithError(err)
}()
refs := make([]*Reference, 0)

View File

@@ -72,11 +72,9 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch)
}
stderr := new(strings.Builder)
err = gitCmd.
WithDir(repo.Path).
WithStdout(stdoutWriter).
WithStderr(stderr).
WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error {
_ = stdoutWriter.Close()
scanner := bufio.NewScanner(stdoutReader)
@@ -146,9 +144,9 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string)
_ = stdoutReader.Close()
return nil
}).
Run(repo.Ctx)
RunWithStderr(repo.Ctx)
if err != nil {
return nil, fmt.Errorf("Failed to get GetCodeActivityStats for repository.\nError: %w\nStderr: %s", err, stderr)
return nil, fmt.Errorf("GetCodeActivityStats: %w", err)
}
return stats, nil

View File

@@ -118,7 +118,6 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
stdoutReader, stdoutWriter := io.Pipe()
defer stdoutReader.Close()
defer stdoutWriter.Close()
stderr := strings.Builder{}
go func() {
err := gitcmd.NewCommand("for-each-ref").
@@ -126,13 +125,8 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
AddArguments("--sort", "-*creatordate", "refs/tags").
WithDir(repo.Path).
WithStdout(stdoutWriter).
WithStderr(&stderr).
Run(repo.Ctx)
if err != nil {
_ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, stderr.String()))
} else {
_ = stdoutWriter.Close()
}
RunWithStderr(repo.Ctx)
_ = stdoutWriter.CloseWithError(err)
}()
var tags []*Tag

View File

@@ -58,16 +58,12 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt
cmd.AddArguments("--no-gpg-sign")
}
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
err := cmd.WithEnv(env).
stdout, _, err := cmd.WithEnv(env).
WithDir(repo.Path).
WithStdin(messageBytes).
WithStdout(stdout).
WithStderr(stderr).
Run(repo.Ctx)
RunStdString(repo.Ctx)
if err != nil {
return nil, gitcmd.ConcatenateError(err, stderr.String())
return nil, err
}
return NewIDFromString(strings.TrimSpace(stdout.String()))
return NewIDFromString(strings.TrimSpace(stdout))
}