@@ -7,81 +7,155 @@ package git
77import (
88 "bytes"
99 "context"
10- "fmt "
10+ "errors "
1111 "io"
1212 "os"
13- "os/exec "
13+ "strconv "
1414 "strings"
1515 "time"
16- )
1716
18- // Command contains the name, arguments and environment variables of a command.
19- type Command struct {
20- name string
21- args []string
22- envs []string
23- ctx context.Context
24- }
17+ "github.com/sourcegraph/run"
18+ )
2519
26- // CommandOptions contains options for running a command.
20+ // CommandOptions contains additional options for running a Git command.
2721type CommandOptions struct {
28- Args []string
2922 Envs []string
3023}
3124
32- // String returns the string representation of the command.
33- func (c * Command ) String () string {
34- if len (c .args ) == 0 {
35- return c .name
25+ // DefaultTimeout is the default timeout duration for all commands. It is
26+ // applied when the context does not already have a deadline.
27+ const DefaultTimeout = time .Minute
28+
29+ // cmd builds a *run.Command for git with the given arguments, environment
30+ // variables and working directory. DefaultTimeout will be applied if the context
31+ // does not already have a deadline.
32+ func cmd (ctx context.Context , dir string , args []string , envs []string ) (* run.Command , context.CancelFunc ) {
33+ cancel := func () {}
34+ if _ , ok := ctx .Deadline (); ! ok {
35+ var timeoutCancel context.CancelFunc
36+ ctx , timeoutCancel = context .WithTimeout (ctx , DefaultTimeout )
37+ cancel = timeoutCancel
3638 }
37- return fmt .Sprintf ("%s %s" , c .name , strings .Join (c .args , " " ))
38- }
3939
40- // NewCommand creates and returns a new Command with given arguments for "git".
41- func NewCommand (ctx context.Context , args ... string ) * Command {
42- return & Command {
43- name : "git" ,
44- args : args ,
45- ctx : ctx ,
40+ // run.Cmd joins all parts into a single string and then shell-parses it. We must
41+ // quote each argument so that special characters (spaces, quotes, angle
42+ // brackets, etc.) are preserved correctly.
43+ parts := make ([]string , 0 , 1 + len (args ))
44+ parts = append (parts , "git" )
45+ for _ , arg := range args {
46+ parts = append (parts , run .Arg (arg ))
4647 }
47- }
4848
49- // AddArgs appends given arguments to the command.
50- func (c * Command ) AddArgs (args ... string ) * Command {
51- c .args = append (c .args , args ... )
52- return c
49+ c := run .Cmd (ctx , parts ... )
50+ if dir != "" {
51+ c = c .Dir (dir )
52+ }
53+ if len (envs ) > 0 {
54+ c = c .Environ (append (os .Environ (), envs ... ))
55+ }
56+ return c , cancel
5357}
5458
55- // AddEnvs appends given environment variables to the command.
56- func (c * Command ) AddEnvs (envs ... string ) * Command {
57- c .envs = append (c .envs , envs ... )
58- return c
59- }
59+ // exec executes a git command in the given directory and returns stdout as
60+ // bytes. Stderr is included in the error message on failure. DefaultTimeout will
61+ // be applied if the context does not already have a deadline. It returns
62+ // ErrExecTimeout if the execution was timed out.
63+ func exec (ctx context.Context , dir string , args []string , envs []string ) ([]byte , error ) {
64+ c , cancel := cmd (ctx , dir , args , envs )
65+ defer cancel ()
66+
67+ var logBuf * bytes.Buffer
68+ if logOutput != nil {
69+ logBuf = new (bytes.Buffer )
70+ logBuf .Grow (512 )
71+ defer func () {
72+ log (dir , args , logBuf .Bytes ())
73+ }()
74+ }
75+
76+ // Use Stream to a buffer to preserve raw bytes (including NUL bytes from
77+ // commands like "ls-tree -z"). The String/Lines methods process output
78+ // line-by-line which corrupts binary-ish output.
79+ stdout := new (bytes.Buffer )
80+ err := c .StdOut ().Run ().Stream (stdout )
81+
82+ // Capture (partial) stdout for logging even on error, so failed commands produce
83+ // a useful log entry rather than an empty one.
84+ if logOutput != nil {
85+ data := stdout .Bytes ()
86+ limit := len (data )
87+ if limit > 512 {
88+ limit = 512
89+ }
90+ logBuf .Write (data [:limit ])
91+ if len (data ) > 512 {
92+ logBuf .WriteString ("... (more omitted)" )
93+ }
94+ }
6095
61- // WithContext returns a new Command with the given context.
62- func ( c Command ) WithContext ( ctx context. Context ) * Command {
63- c . ctx = ctx
64- return & c
96+ if err != nil {
97+ return nil , mapContextError ( err , ctx )
98+ }
99+ return stdout . Bytes (), nil
65100}
66101
67- // AddOptions adds options to the command.
68- func (c * Command ) AddOptions (opts ... CommandOptions ) * Command {
69- for _ , opt := range opts {
70- c .AddArgs (opt .Args ... )
71- c .AddEnvs (opt .Envs ... )
102+ // pipe executes a git command in the given directory, streaming stdout to the
103+ // given io.Writer.
104+ func pipe (ctx context.Context , dir string , args []string , envs []string , stdout io.Writer ) error {
105+ c , cancel := cmd (ctx , dir , args , envs )
106+ defer cancel ()
107+
108+ var buf * bytes.Buffer
109+ w := stdout
110+ if logOutput != nil {
111+ buf = new (bytes.Buffer )
112+ buf .Grow (512 )
113+ w = & limitDualWriter {
114+ W : buf ,
115+ N : int64 (buf .Cap ()),
116+ w : stdout ,
117+ }
118+
119+ defer func () {
120+ log (dir , args , buf .Bytes ())
121+ }()
72122 }
73- return c
123+
124+ streamErr := c .StdOut ().Run ().Stream (w )
125+ if streamErr != nil {
126+ return mapContextError (streamErr , ctx )
127+ }
128+ return nil
74129}
75130
76- // AddCommitter appends given committer to the command.
77- func (c * Command ) AddCommitter (committer * Signature ) * Command {
78- c .AddEnvs ("GIT_COMMITTER_NAME=" + committer .Name , "GIT_COMMITTER_EMAIL=" + committer .Email )
79- return c
131+ // committerEnvs returns environment variables for setting the Git committer.
132+ func committerEnvs (committer * Signature ) []string {
133+ return []string {
134+ "GIT_COMMITTER_NAME=" + committer .Name ,
135+ "GIT_COMMITTER_EMAIL=" + committer .Email ,
136+ }
80137}
81138
82- // DefaultTimeout is the default timeout duration for all commands. It is
83- // applied when the context does not already have a deadline.
84- const DefaultTimeout = time .Minute
139+ // log logs a git command execution with its output.
140+ func log (dir string , args []string , output []byte ) {
141+ cmdStr := "git"
142+ if len (args ) > 0 {
143+ quoted := make ([]string , len (args ))
144+ for i , a := range args {
145+ if strings .ContainsAny (a , " \t \n \" '\\ <>" ) {
146+ quoted [i ] = strconv .Quote (a )
147+ } else {
148+ quoted [i ] = a
149+ }
150+ }
151+ cmdStr = "git " + strings .Join (quoted , " " )
152+ }
153+ if len (dir ) == 0 {
154+ logf ("%s\n %s" , cmdStr , output )
155+ } else {
156+ logf ("%s: %s\n %s" , dir , cmdStr , output )
157+ }
158+ }
85159
86160// A limitDualWriter writes to W but limits the amount of data written to just N
87161// bytes. On the other hand, it passes everything to w.
@@ -111,134 +185,25 @@ func (w *limitDualWriter) Write(p []byte) (int, error) {
111185 return w .w .Write (p )
112186}
113187
114- // RunInDirOptions contains options for running a command in a directory.
115- type RunInDirOptions struct {
116- // Stdin is the input to the command.
117- Stdin io.Reader
118- // Stdout is the outputs from the command.
119- Stdout io.Writer
120- // Stderr is the error output from the command.
121- Stderr io.Writer
122- }
123-
124- // RunInDirWithOptions executes the command in given directory and options. It
125- // pipes stdin from supplied io.Reader, and pipes stdout and stderr to supplied
126- // io.Writer. If the command's context does not have a deadline, DefaultTimeout
127- // will be applied automatically. It returns an ErrExecTimeout if the execution
128- // was timed out.
129- func (c * Command ) RunInDirWithOptions (dir string , opts ... RunInDirOptions ) (err error ) {
130- var opt RunInDirOptions
131- if len (opts ) > 0 {
132- opt = opts [0 ]
133- }
134-
135- buf := new (bytes.Buffer )
136- w := opt .Stdout
137- if logOutput != nil {
138- buf .Grow (512 )
139- w = & limitDualWriter {
140- W : buf ,
141- N : int64 (buf .Cap ()),
142- w : opt .Stdout ,
143- }
144- }
145-
146- defer func () {
147- if len (dir ) == 0 {
148- log ("%s\n %s" , c , buf .Bytes ())
149- } else {
150- log ("%s: %s\n %s" , dir , c , buf .Bytes ())
151- }
152- }()
153-
154- ctx := c .ctx
188+ // mapContextError maps context errors to the appropriate sentinel errors used
189+ // by this package.
190+ func mapContextError (err error , ctx context.Context ) error {
155191 if ctx == nil {
156- ctx = context .Background ()
157- }
158-
159- // Apply default timeout if the context doesn't already have a deadline.
160- if _ , ok := ctx .Deadline (); ! ok {
161- var cancel context.CancelFunc
162- ctx , cancel = context .WithTimeout (ctx , DefaultTimeout )
163- defer cancel ()
164- }
165-
166- cmd := exec .CommandContext (ctx , c .name , c .args ... )
167- if len (c .envs ) > 0 {
168- cmd .Env = append (os .Environ (), c .envs ... )
169- }
170- cmd .Dir = dir
171- cmd .Stdin = opt .Stdin
172- cmd .Stdout = w
173- cmd .Stderr = opt .Stderr
174- if err = cmd .Start (); err != nil {
175- if ctx .Err () == context .DeadlineExceeded {
176- return ErrExecTimeout
177- } else if ctx .Err () != nil {
178- return ctx .Err ()
179- }
180192 return err
181193 }
182-
183- result := make (chan error )
184- go func () {
185- result <- cmd .Wait ()
186- }()
187-
188- select {
189- case <- ctx .Done ():
190- // Kill the process before waiting so cancellation is enforced promptly.
191- if cmd .Process != nil {
192- _ = cmd .Process .Kill ()
193- }
194- <- result
195-
196- if ctx .Err () == context .DeadlineExceeded {
194+ if ctxErr := ctx .Err (); ctxErr != nil {
195+ if errors .Is (ctxErr , context .DeadlineExceeded ) {
197196 return ErrExecTimeout
198197 }
199- return ctx .Err ()
200- case err = <- result :
201- // Normalize errors when the context may have expired around the same time.
202- if err != nil {
203- if ctxErr := ctx .Err (); ctxErr != nil {
204- if ctxErr == context .DeadlineExceeded {
205- return ErrExecTimeout
206- }
207- return ctxErr
208- }
209- }
210- return err
198+ return ctxErr
211199 }
212-
213- }
214-
215- // RunInDirPipeline executes the command in given directory. It pipes stdout and
216- // stderr to supplied io.Writer.
217- func (c * Command ) RunInDirPipeline (stdout , stderr io.Writer , dir string ) error {
218- return c .RunInDirWithOptions (dir , RunInDirOptions {
219- Stdin : nil ,
220- Stdout : stdout ,
221- Stderr : stderr ,
222- })
223- }
224-
225- // RunInDir executes the command in given directory. It returns stdout and error
226- // (combined with stderr).
227- func (c * Command ) RunInDir (dir string ) ([]byte , error ) {
228- stdout := new (bytes.Buffer )
229- stderr := new (bytes.Buffer )
230- if err := c .RunInDirPipeline (stdout , stderr , dir ); err != nil {
231- return nil , concatenateError (err , stderr .String ())
232- }
233- return stdout .Bytes (), nil
200+ return err
234201}
235202
236- // Run executes the command in working directory. It returns stdout and
237- // error (combined with stderr).
238- func (c * Command ) Run () ([]byte , error ) {
239- stdout , err := c .RunInDir ("" )
240- if err != nil {
241- return nil , err
242- }
243- return stdout , nil
203+ // isExitStatus reports whether err represents a specific process exit status
204+ // code, using the run.ExitCoder interface provided by sourcegraph/run.
205+ func isExitStatus (err error , code int ) bool {
206+ var exitCoder run.ExitCoder
207+ ok := errors .As (err , & exitCoder )
208+ return ok && exitCoder .ExitCode () == code
244209}
0 commit comments