2021-10-20 22:21:16 +11:00
package oscommands
import (
"os/exec"
2023-05-21 17:00:29 +10:00
"strings"
2022-08-07 09:44:50 +10:00
2023-07-09 11:32:27 +10:00
"github.com/jesseduffield/gocui"
2023-05-21 17:00:29 +10:00
"github.com/samber/lo"
2022-08-07 09:44:50 +10:00
"github.com/sasha-s/go-deadlock"
2021-10-20 22:21:16 +11:00
)
// A command object is a general way to represent a command to be run on the
2021-12-29 14:33:38 +11:00
// command line.
2021-10-20 22:21:16 +11:00
type ICmdObj interface {
GetCmd ( ) * exec . Cmd
2021-12-30 13:11:58 +11:00
// outputs string representation of command. Note that if the command was built
// using NewFromArgs, the output won't be quite the same as what you would type
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
2021-10-20 22:21:16 +11:00
ToString ( ) string
2022-01-05 12:01:59 +11:00
2023-05-21 17:00:29 +10:00
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
Args ( ) [ ] string
2021-10-20 22:21:16 +11:00
AddEnvVars ( ... string ) ICmdObj
2021-12-07 21:59:36 +11:00
GetEnvVars ( ) [ ] string
2021-12-29 14:33:38 +11:00
2021-12-30 11:22:29 +11:00
// runs the command and returns an error if any
2021-12-29 14:33:38 +11:00
Run ( ) error
2021-12-30 11:22:29 +11:00
// runs the command and returns the output as a string, and an error if any
2021-12-29 14:33:38 +11:00
RunWithOutput ( ) ( string , error )
2022-08-02 08:32:28 +09:00
// runs the command and returns stdout and stderr as a string, and an error if any
RunWithOutputs ( ) ( string , string , error )
2021-12-30 11:22:29 +11:00
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
RunAndProcessLines ( onLine func ( line string ) ( bool , error ) ) error
2021-12-29 14:33:38 +11:00
2022-01-05 12:01:59 +11:00
// Be calling DontLog(), we're saying that once we call Run(), we don't want to
// log the command in the UI (it'll still be logged in the log file). The general rule
// is that if a command doesn't change the git state (e.g. read commands like `git diff`)
// then we don't want to log it. If we are changing something (e.g. `git add .`) then
// we do. The only exception is if we're running a command in the background periodically
// like `git fetch`, which technically does mutate stuff but isn't something we need
// to notify the user about.
2022-01-05 11:57:32 +11:00
DontLog ( ) ICmdObj
2022-01-05 12:01:59 +11:00
// This returns false if DontLog() was called
2022-01-05 11:57:32 +11:00
ShouldLog ( ) bool
2022-01-02 10:34:33 +11:00
2022-01-19 18:32:27 +11:00
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
StreamOutput ( ) ICmdObj
// returns true if StreamOutput() was called
ShouldStreamOutput ( ) bool
// if you call this before ShouldStreamOutput we'll consider an error with no
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
// but adding support is trivial)
IgnoreEmptyError ( ) ICmdObj
// returns true if IgnoreEmptyError() was called
ShouldIgnoreEmptyError ( ) bool
2023-07-09 21:09:52 +10:00
PromptOnCredentialRequest ( task gocui . Task ) ICmdObj
2022-01-02 10:34:33 +11:00
FailOnCredentialRequest ( ) ICmdObj
2022-08-07 09:44:50 +10:00
WithMutex ( mutex * deadlock . Mutex ) ICmdObj
Mutex ( ) * deadlock . Mutex
2022-01-16 14:46:53 +11:00
2022-01-02 10:34:33 +11:00
GetCredentialStrategy ( ) CredentialStrategy
2023-07-09 21:09:52 +10:00
GetTask ( ) gocui . Task
2023-07-10 18:52:08 +10:00
Clone ( ) ICmdObj
2021-10-20 22:21:16 +11:00
}
type CmdObj struct {
2023-05-21 17:00:29 +10:00
// the secureexec package will swap out the first arg with the full path to the binary,
// so we store these args separately so that ToString() will output the original
args [ ] string
cmd * exec . Cmd
2021-12-29 14:33:38 +11:00
2022-01-05 11:57:32 +11:00
runner ICmdObjRunner
2022-01-19 18:32:27 +11:00
// see DontLog()
2022-01-05 11:57:32 +11:00
dontLog bool
2022-01-02 10:34:33 +11:00
2022-01-19 18:32:27 +11:00
// see StreamOutput()
streamOutput bool
// see IgnoreEmptyError()
ignoreEmptyError bool
2022-01-02 10:34:33 +11:00
// if set to true, it means we might be asked to enter a username/password by this command.
credentialStrategy CredentialStrategy
2023-07-09 21:09:52 +10:00
task gocui . Task
2022-01-16 14:46:53 +11:00
// can be set so that we don't run certain commands simultaneously
2022-08-07 09:44:50 +10:00
mutex * deadlock . Mutex
2021-10-20 22:21:16 +11:00
}
2022-01-02 10:34:33 +11:00
type CredentialStrategy int
const (
// do not expect a credential request. If we end up getting one
// we'll be in trouble because the command will hang indefinitely
NONE CredentialStrategy = iota
// expect a credential request and if we get one, prompt the user to enter their username/password
PROMPT
// in this case we will check for a credential request (i.e. the command pauses to ask for
// username/password) and if we get one, we just submit a newline, forcing the
// command to fail. We use this e.g. for a background `git fetch` to prevent it
// from hanging indefinitely.
FAIL
)
var _ ICmdObj = & CmdObj { }
2021-10-20 22:21:16 +11:00
func ( self * CmdObj ) GetCmd ( ) * exec . Cmd {
return self . cmd
}
func ( self * CmdObj ) ToString ( ) string {
2023-05-21 17:00:29 +10:00
// if a given arg contains a space, we need to wrap it in quotes
quotedArgs := lo . Map ( self . args , func ( arg string , _ int ) string {
if strings . Contains ( arg , " " ) {
return ` " ` + arg + ` " `
}
return arg
} )
return strings . Join ( quotedArgs , " " )
}
func ( self * CmdObj ) Args ( ) [ ] string {
return self . args
2021-10-20 22:21:16 +11:00
}
func ( self * CmdObj ) AddEnvVars ( vars ... string ) ICmdObj {
self . cmd . Env = append ( self . cmd . Env , vars ... )
return self
}
2021-12-07 21:59:36 +11:00
func ( self * CmdObj ) GetEnvVars ( ) [ ] string {
return self . cmd . Env
}
2021-12-29 14:33:38 +11:00
2022-01-05 11:57:32 +11:00
func ( self * CmdObj ) DontLog ( ) ICmdObj {
self . dontLog = true
2021-12-29 14:33:38 +11:00
return self
}
2022-01-05 11:57:32 +11:00
func ( self * CmdObj ) ShouldLog ( ) bool {
return ! self . dontLog
}
2022-01-19 18:32:27 +11:00
func ( self * CmdObj ) StreamOutput ( ) ICmdObj {
self . streamOutput = true
return self
}
func ( self * CmdObj ) ShouldStreamOutput ( ) bool {
return self . streamOutput
}
func ( self * CmdObj ) IgnoreEmptyError ( ) ICmdObj {
self . ignoreEmptyError = true
return self
}
2022-08-07 09:44:50 +10:00
func ( self * CmdObj ) Mutex ( ) * deadlock . Mutex {
2022-01-16 14:46:53 +11:00
return self . mutex
}
2022-08-07 09:44:50 +10:00
func ( self * CmdObj ) WithMutex ( mutex * deadlock . Mutex ) ICmdObj {
2022-01-16 14:46:53 +11:00
self . mutex = mutex
return self
}
2022-01-19 18:32:27 +11:00
func ( self * CmdObj ) ShouldIgnoreEmptyError ( ) bool {
return self . ignoreEmptyError
}
2021-12-29 14:33:38 +11:00
func ( self * CmdObj ) Run ( ) error {
return self . runner . Run ( self )
}
func ( self * CmdObj ) RunWithOutput ( ) ( string , error ) {
return self . runner . RunWithOutput ( self )
}
2022-08-02 08:32:28 +09:00
func ( self * CmdObj ) RunWithOutputs ( ) ( string , string , error ) {
return self . runner . RunWithOutputs ( self )
}
2021-12-30 11:22:29 +11:00
func ( self * CmdObj ) RunAndProcessLines ( onLine func ( line string ) ( bool , error ) ) error {
return self . runner . RunAndProcessLines ( self , onLine )
2021-12-29 14:33:38 +11:00
}
2022-01-02 10:34:33 +11:00
2023-07-09 21:09:52 +10:00
func ( self * CmdObj ) PromptOnCredentialRequest ( task gocui . Task ) ICmdObj {
2022-01-02 10:34:33 +11:00
self . credentialStrategy = PROMPT
2023-07-09 11:32:27 +10:00
self . task = task
2022-01-02 10:34:33 +11:00
return self
}
func ( self * CmdObj ) FailOnCredentialRequest ( ) ICmdObj {
self . credentialStrategy = FAIL
return self
}
func ( self * CmdObj ) GetCredentialStrategy ( ) CredentialStrategy {
return self . credentialStrategy
}
2023-07-09 11:32:27 +10:00
2023-07-09 21:09:52 +10:00
func ( self * CmdObj ) GetTask ( ) gocui . Task {
2023-07-09 11:32:27 +10:00
return self . task
}
2023-07-10 18:52:08 +10:00
func ( self * CmdObj ) Clone ( ) ICmdObj {
clone := & CmdObj { }
* clone = * self
clone . cmd = cloneCmd ( self . cmd )
return clone
}
func cloneCmd ( cmd * exec . Cmd ) * exec . Cmd {
clone := & exec . Cmd { }
* clone = * cmd
return clone
}