Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ linters:
desc: do not use the go-chi cache package, use gitea's cache system
- pkg: github.com/pkg/errors
desc: use builtin errors package instead
- pkg: github.com/go-ap/errors
desc: use builtin errors package instead
nolintlint:
allow-unused: false
require-explanation: true
Expand Down
2 changes: 1 addition & 1 deletion cmd/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ func runHookPostReceive(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))

// First of all run update-server-info no matter what
if _, _, err := gitcmd.NewCommand("update-server-info").RunStdString(ctx); err != nil {
if err := gitcmd.NewCommand("update-server-info").RunWithStderr(ctx); err != nil {
return fmt.Errorf("failed to call 'git update-server-info': %w", err)
}

Expand Down
4 changes: 1 addition & 3 deletions modules/git/attribute/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 2 additions & 5 deletions modules/git/attribute/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'})
Expand Down
11 changes: 5 additions & 6 deletions modules/git/catfile_batch_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bufio"
"bytes"
"context"
"errors"
"io"
"math"
"strconv"
Expand Down Expand Up @@ -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
Expand All @@ -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)
}()
Expand Down
4 changes: 2 additions & 2 deletions modules/git/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 2 additions & 8 deletions modules/git/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package git

import (
"bufio"
"bytes"
"context"
"fmt"
"io"
Expand Down Expand Up @@ -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
Expand Down
100 changes: 63 additions & 37 deletions modules/git/gitcmd/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -235,8 +237,6 @@ type runOpts struct {
Stdin io.Reader

PipelineFunc func(context.Context, context.CancelFunc) error

callerInfo string
}

func commonBaseEnvs() []string {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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*/
Expand All @@ -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
}

Expand All @@ -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()
}
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
}
}

Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions modules/git/gitcmd/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -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)
}
}
Expand Down
13 changes: 0 additions & 13 deletions modules/git/gitcmd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading