1// Copyright 2015 The Gogs Authors. All rights reserved.
2// Copyright 2016 The Gitea Authors. All rights reserved.
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file.
5
6package git
7
8import (
9	"bytes"
10	"context"
11	"fmt"
12	"io"
13	"os"
14	"os/exec"
15	"strings"
16	"time"
17
18	"code.gitea.io/gitea/modules/log"
19	"code.gitea.io/gitea/modules/process"
20)
21
22var (
23	// GlobalCommandArgs global command args for external package setting
24	GlobalCommandArgs []string
25
26	// defaultCommandExecutionTimeout default command execution timeout duration
27	defaultCommandExecutionTimeout = 360 * time.Second
28)
29
30// DefaultLocale is the default LC_ALL to run git commands in.
31const DefaultLocale = "C"
32
33// Command represents a command with its subcommands or arguments.
34type Command struct {
35	name          string
36	args          []string
37	parentContext context.Context
38	desc          string
39}
40
41func (c *Command) String() string {
42	if len(c.args) == 0 {
43		return c.name
44	}
45	return fmt.Sprintf("%s %s", c.name, strings.Join(c.args, " "))
46}
47
48// NewCommand creates and returns a new Git Command based on given command and arguments.
49func NewCommand(args ...string) *Command {
50	return NewCommandContext(DefaultContext, args...)
51}
52
53// NewCommandContext creates and returns a new Git Command based on given command and arguments.
54func NewCommandContext(ctx context.Context, args ...string) *Command {
55	// Make an explicit copy of GlobalCommandArgs, otherwise append might overwrite it
56	cargs := make([]string, len(GlobalCommandArgs))
57	copy(cargs, GlobalCommandArgs)
58	return &Command{
59		name:          GitExecutable,
60		args:          append(cargs, args...),
61		parentContext: ctx,
62	}
63}
64
65// NewCommandNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args
66func NewCommandNoGlobals(args ...string) *Command {
67	return NewCommandContextNoGlobals(DefaultContext, args...)
68}
69
70// NewCommandContextNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args
71func NewCommandContextNoGlobals(ctx context.Context, args ...string) *Command {
72	return &Command{
73		name:          GitExecutable,
74		args:          args,
75		parentContext: ctx,
76	}
77}
78
79// SetParentContext sets the parent context for this command
80func (c *Command) SetParentContext(ctx context.Context) *Command {
81	c.parentContext = ctx
82	return c
83}
84
85// SetDescription sets the description for this command which be returned on
86// c.String()
87func (c *Command) SetDescription(desc string) *Command {
88	c.desc = desc
89	return c
90}
91
92// AddArguments adds new argument(s) to the command.
93func (c *Command) AddArguments(args ...string) *Command {
94	c.args = append(c.args, args...)
95	return c
96}
97
98// RunInDirTimeoutEnvPipeline executes the command in given directory with given timeout,
99// it pipes stdout and stderr to given io.Writer.
100func (c *Command) RunInDirTimeoutEnvPipeline(env []string, timeout time.Duration, dir string, stdout, stderr io.Writer) error {
101	return c.RunInDirTimeoutEnvFullPipeline(env, timeout, dir, stdout, stderr, nil)
102}
103
104// RunInDirTimeoutEnvFullPipeline executes the command in given directory with given timeout,
105// it pipes stdout and stderr to given io.Writer and passes in an io.Reader as stdin.
106func (c *Command) RunInDirTimeoutEnvFullPipeline(env []string, timeout time.Duration, dir string, stdout, stderr io.Writer, stdin io.Reader) error {
107	return c.RunInDirTimeoutEnvFullPipelineFunc(env, timeout, dir, stdout, stderr, stdin, nil)
108}
109
110// RunInDirTimeoutEnvFullPipelineFunc executes the command in given directory with given timeout,
111// it pipes stdout and stderr to given io.Writer and passes in an io.Reader as stdin. Between cmd.Start and cmd.Wait the passed in function is run.
112func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time.Duration, dir string, stdout, stderr io.Writer, stdin io.Reader, fn func(context.Context, context.CancelFunc) error) error {
113	return c.RunWithContext(&RunContext{
114		Env:          env,
115		Timeout:      timeout,
116		Dir:          dir,
117		Stdout:       stdout,
118		Stderr:       stderr,
119		Stdin:        stdin,
120		PipelineFunc: fn,
121	})
122}
123
124// RunContext represents parameters to run the command
125type RunContext struct {
126	Env            []string
127	Timeout        time.Duration
128	Dir            string
129	Stdout, Stderr io.Writer
130	Stdin          io.Reader
131	PipelineFunc   func(context.Context, context.CancelFunc) error
132}
133
134// RunWithContext run the command with context
135func (c *Command) RunWithContext(rc *RunContext) error {
136	if rc.Timeout == -1 {
137		rc.Timeout = defaultCommandExecutionTimeout
138	}
139
140	if len(rc.Dir) == 0 {
141		log.Debug("%s", c)
142	} else {
143		log.Debug("%s: %v", rc.Dir, c)
144	}
145
146	desc := c.desc
147	if desc == "" {
148		desc = fmt.Sprintf("%s %s [repo_path: %s]", c.name, strings.Join(c.args, " "), rc.Dir)
149	}
150
151	ctx, cancel, finished := process.GetManager().AddContextTimeout(c.parentContext, rc.Timeout, desc)
152	defer finished()
153
154	cmd := exec.CommandContext(ctx, c.name, c.args...)
155	if rc.Env == nil {
156		cmd.Env = os.Environ()
157	} else {
158		cmd.Env = rc.Env
159	}
160
161	cmd.Env = append(
162		cmd.Env,
163		fmt.Sprintf("LC_ALL=%s", DefaultLocale),
164		// avoid prompting for credentials interactively, supported since git v2.3
165		"GIT_TERMINAL_PROMPT=0",
166		// ignore replace references (https://git-scm.com/docs/git-replace)
167		"GIT_NO_REPLACE_OBJECTS=1",
168	)
169
170	// TODO: verify if this is still needed in golang 1.15
171	if goVersionLessThan115 {
172		cmd.Env = append(cmd.Env, "GODEBUG=asyncpreemptoff=1")
173	}
174	cmd.Dir = rc.Dir
175	cmd.Stdout = rc.Stdout
176	cmd.Stderr = rc.Stderr
177	cmd.Stdin = rc.Stdin
178	if err := cmd.Start(); err != nil {
179		return err
180	}
181
182	if rc.PipelineFunc != nil {
183		err := rc.PipelineFunc(ctx, cancel)
184		if err != nil {
185			cancel()
186			_ = cmd.Wait()
187			return err
188		}
189	}
190
191	if err := cmd.Wait(); err != nil && ctx.Err() != context.DeadlineExceeded {
192		return err
193	}
194
195	return ctx.Err()
196}
197
198// RunInDirTimeoutPipeline executes the command in given directory with given timeout,
199// it pipes stdout and stderr to given io.Writer.
200func (c *Command) RunInDirTimeoutPipeline(timeout time.Duration, dir string, stdout, stderr io.Writer) error {
201	return c.RunInDirTimeoutEnvPipeline(nil, timeout, dir, stdout, stderr)
202}
203
204// RunInDirTimeoutFullPipeline executes the command in given directory with given timeout,
205// it pipes stdout and stderr to given io.Writer, and stdin from the given io.Reader
206func (c *Command) RunInDirTimeoutFullPipeline(timeout time.Duration, dir string, stdout, stderr io.Writer, stdin io.Reader) error {
207	return c.RunInDirTimeoutEnvFullPipeline(nil, timeout, dir, stdout, stderr, stdin)
208}
209
210// RunInDirTimeout executes the command in given directory with given timeout,
211// and returns stdout in []byte and error (combined with stderr).
212func (c *Command) RunInDirTimeout(timeout time.Duration, dir string) ([]byte, error) {
213	return c.RunInDirTimeoutEnv(nil, timeout, dir)
214}
215
216// RunInDirTimeoutEnv executes the command in given directory with given timeout,
217// and returns stdout in []byte and error (combined with stderr).
218func (c *Command) RunInDirTimeoutEnv(env []string, timeout time.Duration, dir string) ([]byte, error) {
219	stdout := new(bytes.Buffer)
220	stderr := new(bytes.Buffer)
221	if err := c.RunInDirTimeoutEnvPipeline(env, timeout, dir, stdout, stderr); err != nil {
222		return nil, ConcatenateError(err, stderr.String())
223	}
224	if stdout.Len() > 0 && log.IsTrace() {
225		tracelen := stdout.Len()
226		if tracelen > 1024 {
227			tracelen = 1024
228		}
229		log.Trace("Stdout:\n %s", stdout.Bytes()[:tracelen])
230	}
231	return stdout.Bytes(), nil
232}
233
234// RunInDirPipeline executes the command in given directory,
235// it pipes stdout and stderr to given io.Writer.
236func (c *Command) RunInDirPipeline(dir string, stdout, stderr io.Writer) error {
237	return c.RunInDirFullPipeline(dir, stdout, stderr, nil)
238}
239
240// RunInDirFullPipeline executes the command in given directory,
241// it pipes stdout and stderr to given io.Writer.
242func (c *Command) RunInDirFullPipeline(dir string, stdout, stderr io.Writer, stdin io.Reader) error {
243	return c.RunInDirTimeoutFullPipeline(-1, dir, stdout, stderr, stdin)
244}
245
246// RunInDirBytes executes the command in given directory
247// and returns stdout in []byte and error (combined with stderr).
248func (c *Command) RunInDirBytes(dir string) ([]byte, error) {
249	return c.RunInDirTimeout(-1, dir)
250}
251
252// RunInDir executes the command in given directory
253// and returns stdout in string and error (combined with stderr).
254func (c *Command) RunInDir(dir string) (string, error) {
255	return c.RunInDirWithEnv(dir, nil)
256}
257
258// RunInDirWithEnv executes the command in given directory
259// and returns stdout in string and error (combined with stderr).
260func (c *Command) RunInDirWithEnv(dir string, env []string) (string, error) {
261	stdout, err := c.RunInDirTimeoutEnv(env, -1, dir)
262	if err != nil {
263		return "", err
264	}
265	return string(stdout), nil
266}
267
268// RunTimeout executes the command in default working directory with given timeout,
269// and returns stdout in string and error (combined with stderr).
270func (c *Command) RunTimeout(timeout time.Duration) (string, error) {
271	stdout, err := c.RunInDirTimeout(timeout, "")
272	if err != nil {
273		return "", err
274	}
275	return string(stdout), nil
276}
277
278// Run executes the command in default working directory
279// and returns stdout in string and error (combined with stderr).
280func (c *Command) Run() (string, error) {
281	return c.RunTimeout(-1)
282}
283