@@ -40,6 +40,7 @@ const DefaultLocale = "C"
4040
4141// Command represents a command with its subcommands or arguments.
4242type Command struct {
43+ callerInfo string
4344 prog string
4445 args []string
4546 preErrors []error
@@ -52,9 +53,10 @@ type Command struct {
5253 cmdFinished context.CancelFunc
5354 cmdStartTime time.Time
5455
55- cmdStdinWriter * io.WriteCloser
56- cmdStdoutReader * io.ReadCloser
57- cmdStderrReader * io.ReadCloser
56+ cmdStdinWriter * io.WriteCloser
57+ cmdStdoutReader * io.ReadCloser
58+ cmdStderrReader * io.ReadCloser
59+ cmdManagedStderr * bytes.Buffer
5860}
5961
6062func logArgSanitize (arg string ) string {
@@ -221,7 +223,7 @@ type runOpts struct {
221223 // The correct approach is to use `--git-dir" global argument
222224 Dir string
223225
224- Stdout , Stderr io.Writer
226+ Stdout io.Writer
225227
226228 // Stdin is used for passing input to the command
227229 // The caller must make sure the Stdin writer is closed properly to finish the Run function.
@@ -235,8 +237,6 @@ type runOpts struct {
235237 Stdin io.Reader
236238
237239 PipelineFunc func (context.Context , context.CancelFunc ) error
238-
239- callerInfo string
240240}
241241
242242func commonBaseEnvs () []string {
@@ -310,12 +310,6 @@ func (c *Command) WithStderrReader(r *io.ReadCloser) *Command {
310310 return c
311311}
312312
313- // WithStderr is deprecated, use WithStderrReader instead
314- func (c * Command ) WithStderr (stderr io.Writer ) * Command {
315- c .opts .Stderr = stderr
316- return c
317- }
318-
319313func (c * Command ) WithStdinWriter (w * io.WriteCloser ) * Command {
320314 c .cmdStdinWriter = w
321315 return c
@@ -343,11 +337,11 @@ func (c *Command) WithUseContextTimeout(useContextTimeout bool) *Command {
343337// then you can to call this function in GeneralWrapperFunc to set the caller info of FeatureFunc.
344338// The caller info can only be set once.
345339func (c * Command ) WithParentCallerInfo (optInfo ... string ) * Command {
346- if c .opts . callerInfo != "" {
340+ if c .callerInfo != "" {
347341 return c
348342 }
349343 if len (optInfo ) > 0 {
350- c .opts . callerInfo = optInfo [0 ]
344+ c .callerInfo = optInfo [0 ]
351345 return c
352346 }
353347 skip := 1 /*parent "wrap/run" functions*/ + 1 /*this function*/
@@ -356,7 +350,7 @@ func (c *Command) WithParentCallerInfo(optInfo ...string) *Command {
356350 if pos := strings .LastIndex (callerInfo , "/" ); pos >= 0 {
357351 callerInfo = callerInfo [pos + 1 :]
358352 }
359- c .opts . callerInfo = callerInfo
353+ c .callerInfo = callerInfo
360354 return c
361355}
362356
@@ -372,7 +366,7 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
372366 safeClosePtrCloser (c .cmdStdoutReader )
373367 safeClosePtrCloser (c .cmdStderrReader )
374368 safeClosePtrCloser (c .cmdStdinWriter )
375- // if no error, cmdFinished will be called in "Wait" function
369+ // if error occurs, we must also finish the task, other , cmdFinished will be called in "Wait" function
376370 if c .cmdFinished != nil {
377371 c .cmdFinished ()
378372 }
@@ -393,16 +387,16 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
393387 }
394388
395389 cmdLogString := c .LogString ()
396- if c .opts . callerInfo == "" {
390+ if c .callerInfo == "" {
397391 c .WithParentCallerInfo ()
398392 }
399393 // these logs are for debugging purposes only, so no guarantee of correctness or stability
400- desc := fmt .Sprintf ("git.Run(by:%s, repo:%s): %s" , c .opts . callerInfo , logArgSanitize (c .opts .Dir ), cmdLogString )
394+ desc := fmt .Sprintf ("git.Run(by:%s, repo:%s): %s" , c .callerInfo , logArgSanitize (c .opts .Dir ), cmdLogString )
401395 log .Debug ("git.Command: %s" , desc )
402396
403397 _ , span := gtprof .GetTracer ().Start (ctx , gtprof .TraceSpanGitRun )
404398 defer span .End ()
405- span .SetAttributeString (gtprof .TraceAttrFuncCaller , c .opts . callerInfo )
399+ span .SetAttributeString (gtprof .TraceAttrFuncCaller , c .callerInfo )
406400 span .SetAttributeString (gtprof .TraceAttrGitCommand , cmdLogString )
407401
408402 if c .opts .UseContextTimeout {
@@ -425,7 +419,6 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
425419 cmd .Env = append (cmd .Env , CommonGitCmdEnvs ()... )
426420 cmd .Dir = c .opts .Dir
427421 cmd .Stdout = c .opts .Stdout
428- cmd .Stderr = c .opts .Stderr
429422 cmd .Stdin = c .opts .Stdin
430423
431424 if _ , err := safeAssignPipe (c .cmdStdinWriter , cmd .StdinPipe ); err != nil {
@@ -437,19 +430,32 @@ func (c *Command) Start(ctx context.Context) (retErr error) {
437430 if _ , err := safeAssignPipe (c .cmdStderrReader , cmd .StderrPipe ); err != nil {
438431 return err
439432 }
433+
434+ if c .cmdManagedStderr != nil {
435+ if cmd .Stderr != nil {
436+ panic ("CombineStderr needs managed (but not caller-provided) stderr pipe" )
437+ }
438+ cmd .Stderr = c .cmdManagedStderr
439+ }
440440 return cmd .Start ()
441441}
442442
443443func (c * Command ) Wait () error {
444- defer c .cmdFinished ()
444+ defer func () {
445+ safeClosePtrCloser (c .cmdStdoutReader )
446+ safeClosePtrCloser (c .cmdStderrReader )
447+ safeClosePtrCloser (c .cmdStdinWriter )
448+ c .cmdFinished ()
449+ }()
450+
445451 cmd , ctx , cancel := c .cmd , c .cmdCtx , c .cmdCancel
446452
447453 if c .opts .PipelineFunc != nil {
448454 err := c .opts .PipelineFunc (ctx , cancel )
449455 if err != nil {
450456 cancel ()
451- _ = cmd .Wait ()
452- return err
457+ errWait : = cmd .Wait ()
458+ return errors . Join ( err , errWait )
453459 }
454460 }
455461
@@ -472,6 +478,34 @@ func (c *Command) Wait() error {
472478 return errCause
473479}
474480
481+ func (c * Command ) StartWithStderr (ctx context.Context ) RunStdError {
482+ c .cmdManagedStderr = & bytes.Buffer {}
483+ err := c .Start (ctx )
484+ if err != nil {
485+ return & runStdError {err : err }
486+ }
487+ return nil
488+ }
489+
490+ func (c * Command ) WaitWithStderr () RunStdError {
491+ if c .cmdManagedStderr == nil {
492+ panic ("CombineStderr needs managed (but not caller-provided) stderr pipe" )
493+ }
494+ errWait := c .Wait ()
495+ if errWait == nil {
496+ // if no exec error but only stderr output, the error is still saved in "c.cmdManagedStderr" and can be read later
497+ return nil
498+ }
499+ return & runStdError {err : errWait , stderr : util .UnsafeBytesToString (c .cmdManagedStderr .Bytes ())}
500+ }
501+
502+ func (c * Command ) RunWithStderr (ctx context.Context ) RunStdError {
503+ if err := c .StartWithStderr (ctx ); err != nil {
504+ return & runStdError {err : err }
505+ }
506+ return c .WaitWithStderr ()
507+ }
508+
475509func (c * Command ) Run (ctx context.Context ) (err error ) {
476510 if err = c .Start (ctx ); err != nil {
477511 return err
@@ -495,7 +529,7 @@ func (r *runStdError) Error() string {
495529 // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
496530 // But a lof of code only checks `strings.Contains(err.Error(), "git error")`
497531 if r .errMsg == "" {
498- r .errMsg = ConcatenateError ( r .err , r .stderr ). Error ( )
532+ r .errMsg = fmt . Sprintf ( "%s - %s" , r .err . Error (), strings . TrimSpace ( r .stderr ))
499533 }
500534 return r .errMsg
501535}
@@ -543,24 +577,16 @@ func (c *Command) RunStdBytes(ctx context.Context) (stdout, stderr []byte, runEr
543577 return c .WithParentCallerInfo ().runStdBytes (ctx )
544578}
545579
546- func (c * Command ) runStdBytes (ctx context.Context ) ( /*stdout*/ []byte /*stderr*/ , []byte /*runErr*/ , RunStdError ) {
547- if c .opts .Stdout != nil || c .opts . Stderr != nil {
580+ func (c * Command ) runStdBytes (ctx context.Context ) ([]byte , []byte , RunStdError ) {
581+ if c .opts .Stdout != nil || c .cmdStdoutReader != nil || c . cmdStderrReader != nil {
548582 // we must panic here, otherwise there would be bugs if developers set Stdin/Stderr by mistake, and it would be very difficult to debug
549583 panic ("stdout and stderr field must be nil when using RunStdBytes" )
550584 }
551585 stdoutBuf := & bytes.Buffer {}
552- stderrBuf := & bytes.Buffer {}
553586 err := c .WithParentCallerInfo ().
554587 WithStdout (stdoutBuf ).
555- WithStderr (stderrBuf ).
556- Run (ctx )
557- if err != nil {
558- // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message
559- // But a lot of code depends on it, so we have to keep this behavior
560- return nil , stderrBuf .Bytes (), & runStdError {err : err , stderr : util .UnsafeBytesToString (stderrBuf .Bytes ())}
561- }
562- // even if there is no err, there could still be some stderr output
563- return stdoutBuf .Bytes (), stderrBuf .Bytes (), nil
588+ RunWithStderr (ctx )
589+ return stdoutBuf .Bytes (), c .cmdManagedStderr .Bytes (), err
564590}
565591
566592func (c * Command ) DebugKill () {
0 commit comments