1package git23import (4 "bytes"5 "context"6 "errors"7 "io"8 "os"9 "strconv"10 "strings"11 "time"1213 "github.com/sourcegraph/run"14)1516// CommandOptions contains additional options for running a Git command.17type CommandOptions struct {18 Envs []string19}2021// DefaultTimeout is the default timeout duration for all commands. It is22// applied when the context does not already have a deadline.23const DefaultTimeout = time.Minute2425// cmd builds a *run.Command for git with the given arguments, environment26// variables and working directory. DefaultTimeout will be applied if the context27// does not already have a deadline.28func cmd(ctx context.Context, dir string, args []string, envs []string) (*run.Command, context.CancelFunc) {29 cancel := func() {}30 if _, ok := ctx.Deadline(); !ok {31 var timeoutCancel context.CancelFunc32 ctx, timeoutCancel = context.WithTimeout(ctx, DefaultTimeout)33 cancel = timeoutCancel34 }3536 // run.Cmd joins all parts into a single string and then shell-parses it. We must37 // quote each argument so that special characters (spaces, quotes, angle38 // brackets, etc.) are preserved correctly.39 parts := make([]string, 0, 1+len(args))40 parts = append(parts, "git")41 for _, arg := range args {42 parts = append(parts, run.Arg(arg))43 }4445 c := run.Cmd(ctx, parts...)46 if dir != "" {47 c = c.Dir(dir)48 }49 if len(envs) > 0 {50 c = c.Environ(append(os.Environ(), envs...))51 }52 return c, cancel53}5455// exec executes a git command in the given directory and returns stdout as56// bytes. Stderr is included in the error message on failure. DefaultTimeout will57// be applied if the context does not already have a deadline. It returns58// ErrExecTimeout if the execution was timed out.59func exec(ctx context.Context, dir string, args []string, envs []string) ([]byte, error) {60 c, cancel := cmd(ctx, dir, args, envs)61 defer cancel()6263 var logBuf *bytes.Buffer64 if logOutput != nil {65 logBuf = new(bytes.Buffer)66 logBuf.Grow(512)67 defer func() {68 log(dir, args, logBuf.Bytes())69 }()70 }7172 // Use Stream to a buffer to preserve raw bytes (including NUL bytes from73 // commands like "ls-tree -z"). The String/Lines methods process output74 // line-by-line which corrupts binary-ish output.75 stdout := new(bytes.Buffer)76 err := c.StdOut().Run().Stream(stdout)7778 // Capture (partial) stdout for logging even on error, so failed commands produce79 // a useful log entry rather than an empty one.80 if logOutput != nil {81 data := stdout.Bytes()82 limit := len(data)83 if limit > 512 {84 limit = 51285 }86 logBuf.Write(data[:limit])87 if len(data) > 512 {88 logBuf.WriteString("... (more omitted)")89 }90 }9192 if err != nil {93 return nil, mapContextError(err, ctx)94 }95 return stdout.Bytes(), nil96}9798// pipe executes a git command in the given directory, streaming stdout to the99// given io.Writer.100func pipe(ctx context.Context, dir string, args []string, envs []string, stdout io.Writer) error {101 c, cancel := cmd(ctx, dir, args, envs)102 defer cancel()103104 var buf *bytes.Buffer105 w := stdout106 if logOutput != nil {107 buf = new(bytes.Buffer)108 buf.Grow(512)109 w = &limitDualWriter{110 W: buf,111 N: int64(buf.Cap()),112 w: stdout,113 }114115 defer func() {116 log(dir, args, buf.Bytes())117 }()118 }119120 streamErr := c.StdOut().Run().Stream(w)121 if streamErr != nil {122 return mapContextError(streamErr, ctx)123 }124 return nil125}126127// committerEnvs returns environment variables for setting the Git committer.128func committerEnvs(committer *Signature) []string {129 return []string{130 "GIT_COMMITTER_NAME=" + committer.Name,131 "GIT_COMMITTER_EMAIL=" + committer.Email,132 }133}134135// log logs a git command execution with its output.136func log(dir string, args []string, output []byte) {137 cmdStr := "git"138 if len(args) > 0 {139 quoted := make([]string, len(args))140 for i, a := range args {141 if strings.ContainsAny(a, " \t\n\"'\\<>") {142 quoted[i] = strconv.Quote(a)143 } else {144 quoted[i] = a145 }146 }147 cmdStr = "git " + strings.Join(quoted, " ")148 }149 if len(dir) == 0 {150 logf("%s\n%s", cmdStr, output)151 } else {152 logf("%s: %s\n%s", dir, cmdStr, output)153 }154}155156// A limitDualWriter writes to W but limits the amount of data written to just N157// bytes. On the other hand, it passes everything to w.158type limitDualWriter struct {159 W io.Writer // underlying writer160 N int64 // max bytes remaining161 prompted bool162163 w io.Writer164}165166func (w *limitDualWriter) Write(p []byte) (int, error) {167 if w.N > 0 {168 limit := int64(len(p))169 if limit > w.N {170 limit = w.N171 }172 n, _ := w.W.Write(p[:limit])173 w.N -= int64(n)174 }175176 if !w.prompted && w.N <= 0 {177 w.prompted = true178 _, _ = w.W.Write([]byte("... (more omitted)"))179 }180181 return w.w.Write(p)182}183184// mapContextError maps context errors to the appropriate sentinel errors used185// by this package.186func mapContextError(err error, ctx context.Context) error {187 if ctx == nil {188 return err189 }190 if ctxErr := ctx.Err(); ctxErr != nil {191 if errors.Is(ctxErr, context.DeadlineExceeded) {192 return ErrExecTimeout193 }194 return ctxErr195 }196 return err197}198199// isExitStatus reports whether err represents a specific process exit status200// code, using the run.ExitCoder interface provided by sourcegraph/run.201func isExitStatus(err error, code int) bool {202 var exitCoder run.ExitCoder203 ok := errors.As(err, &exitCoder)204 return ok && exitCoder.ExitCode() == code205}