git-module

git clone git://git.lin.moe/fork/git-module.git

  1package git
  2
  3import (
  4	"bytes"
  5	"context"
  6	"errors"
  7	"io"
  8	"os"
  9	"strconv"
 10	"strings"
 11	"time"
 12
 13	"github.com/sourcegraph/run"
 14)
 15
 16// CommandOptions contains additional options for running a Git command.
 17type CommandOptions struct {
 18	Envs []string
 19}
 20
 21// DefaultTimeout is the default timeout duration for all commands. It is
 22// applied when the context does not already have a deadline.
 23const DefaultTimeout = time.Minute
 24
 25// cmd builds a *run.Command for git with the given arguments, environment
 26// variables and working directory. DefaultTimeout will be applied if the context
 27// 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.CancelFunc
 32		ctx, timeoutCancel = context.WithTimeout(ctx, DefaultTimeout)
 33		cancel = timeoutCancel
 34	}
 35
 36	// run.Cmd joins all parts into a single string and then shell-parses it. We must
 37	// quote each argument so that special characters (spaces, quotes, angle
 38	// 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	}
 44
 45	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, cancel
 53}
 54
 55// exec executes a git command in the given directory and returns stdout as
 56// bytes. Stderr is included in the error message on failure. DefaultTimeout will
 57// be applied if the context does not already have a deadline. It returns
 58// 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()
 62
 63	var logBuf *bytes.Buffer
 64	if logOutput != nil {
 65		logBuf = new(bytes.Buffer)
 66		logBuf.Grow(512)
 67		defer func() {
 68			log(dir, args, logBuf.Bytes())
 69		}()
 70	}
 71
 72	// Use Stream to a buffer to preserve raw bytes (including NUL bytes from
 73	// commands like "ls-tree -z"). The String/Lines methods process output
 74	// line-by-line which corrupts binary-ish output.
 75	stdout := new(bytes.Buffer)
 76	err := c.StdOut().Run().Stream(stdout)
 77
 78	// Capture (partial) stdout for logging even on error, so failed commands produce
 79	// 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 = 512
 85		}
 86		logBuf.Write(data[:limit])
 87		if len(data) > 512 {
 88			logBuf.WriteString("... (more omitted)")
 89		}
 90	}
 91
 92	if err != nil {
 93		return nil, mapContextError(err, ctx)
 94	}
 95	return stdout.Bytes(), nil
 96}
 97
 98// pipe executes a git command in the given directory, streaming stdout to the
 99// 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()
103
104	var buf *bytes.Buffer
105	w := stdout
106	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		}
114
115		defer func() {
116			log(dir, args, buf.Bytes())
117		}()
118	}
119
120	streamErr := c.StdOut().Run().Stream(w)
121	if streamErr != nil {
122		return mapContextError(streamErr, ctx)
123	}
124	return nil
125}
126
127// 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}
134
135// 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] = a
145			}
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}
155
156// A limitDualWriter writes to W but limits the amount of data written to just N
157// bytes. On the other hand, it passes everything to w.
158type limitDualWriter struct {
159	W        io.Writer // underlying writer
160	N        int64     // max bytes remaining
161	prompted bool
162
163	w io.Writer
164}
165
166func (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.N
171		}
172		n, _ := w.W.Write(p[:limit])
173		w.N -= int64(n)
174	}
175
176	if !w.prompted && w.N <= 0 {
177		w.prompted = true
178		_, _ = w.W.Write([]byte("... (more omitted)"))
179	}
180
181	return w.w.Write(p)
182}
183
184// mapContextError maps context errors to the appropriate sentinel errors used
185// by this package.
186func mapContextError(err error, ctx context.Context) error {
187	if ctx == nil {
188		return err
189	}
190	if ctxErr := ctx.Err(); ctxErr != nil {
191		if errors.Is(ctxErr, context.DeadlineExceeded) {
192			return ErrExecTimeout
193		}
194		return ctxErr
195	}
196	return err
197}
198
199// isExitStatus reports whether err represents a specific process exit status
200// code, using the run.ExitCoder interface provided by sourcegraph/run.
201func isExitStatus(err error, code int) bool {
202	var exitCoder run.ExitCoder
203	ok := errors.As(err, &exitCoder)
204	return ok && exitCoder.ExitCode() == code
205}