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