start breaking up git struct

This commit is contained in:
Jesse Duffield 2022-01-02 10:34:33 +11:00
parent 4a1d23dc27
commit f503ff1ecb
76 changed files with 2234 additions and 1758 deletions

View file

@ -34,6 +34,11 @@ type ICmdObj interface {
// This returns false if DontLog() was called
ShouldLog() bool
PromptOnCredentialRequest() ICmdObj
FailOnCredentialRequest() ICmdObj
GetCredentialStrategy() CredentialStrategy
}
type CmdObj struct {
@ -44,8 +49,28 @@ type CmdObj struct {
// if set to true, we don't want to log the command to the user.
dontLog bool
// if set to true, it means we might be asked to enter a username/password by this command.
credentialStrategy CredentialStrategy
}
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{}
func (self *CmdObj) GetCmd() *exec.Cmd {
return self.cmd
}
@ -84,3 +109,19 @@ func (self *CmdObj) RunWithOutput() (string, error) {
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
return self.runner.RunAndProcessLines(self, onLine)
}
func (self *CmdObj) PromptOnCredentialRequest() ICmdObj {
self.credentialStrategy = PROMPT
return self
}
func (self *CmdObj) FailOnCredentialRequest() ICmdObj {
self.credentialStrategy = FAIL
return self
}
func (self *CmdObj) GetCredentialStrategy() CredentialStrategy {
return self.credentialStrategy
}

View file

@ -15,18 +15,46 @@ type ICmdObjRunner interface {
}
type cmdObjRunner struct {
log *logrus.Entry
logCmdObj func(ICmdObj)
log *logrus.Entry
guiIO *guiIO
}
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
switch cmdObj.GetCredentialStrategy() {
case PROMPT:
return self.RunCommandWithOutputLive(cmdObj, self.guiIO.promptForCredentialFn)
case FAIL:
return self.RunCommandWithOutputLive(cmdObj, func(s string) string { return "\n" })
}
// we should never land here
return errors.New("runWithCredentialHandling called but cmdObj does not have a a credential strategy")
}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
if cmdObj.GetCredentialStrategy() == NONE {
_, err := self.RunWithOutput(cmdObj)
return err
} else {
return self.runWithCredentialHandling(cmdObj)
}
}
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
self.guiIO.logCommandFn(cmdObj.ToString(), true)
}
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if cmdObj.GetCredentialStrategy() != NONE {
err := self.runWithCredentialHandling(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
}
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
@ -39,6 +67,10 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
}
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
if cmdObj.GetCredentialStrategy() != NONE {
return errors.New("cannot call RunAndProcessLines with credential strategy. If you're seeing this then a contributor to Lazygit has accidentally called this method! Please raise an issue")
}
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}

View file

@ -6,7 +6,7 @@ import (
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform)
osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
return osCmd
}
@ -27,7 +27,7 @@ var dummyPlatform = &Platform{
}
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform)
osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
return osCommand

View file

@ -13,12 +13,12 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// DetectUnamePass detect a username / password / passphrase question in a command
// RunAndDetectCredentialRequest detect a username / password / passphrase question in a command
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUserForCredential func(string) string) error {
func (self *cmdObjRunner) RunAndDetectCredentialRequest(cmdObj ICmdObj, promptUserForCredential func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(cmdObj, writer, func(word string) string {
err := self.RunCommandWithOutputLive(cmdObj, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
@ -37,13 +37,7 @@ func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUser
return ""
})
return errMessage
}
// Due to a lack of pty support on windows we have RunCommandWithOutputLiveWrapper being defined
// separate for windows and other OS's
func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, writer io.Writer, handleOutput func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, cmdObj, writer, handleOutput)
return err
}
type cmdHandler struct {
@ -56,23 +50,22 @@ type cmdHandler struct {
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't write anything to stdin
func RunCommandWithOutputLiveAux(
c *OSCommand,
func (self *cmdObjRunner) RunCommandWithOutputLiveAux(
cmdObj ICmdObj,
writer io.Writer,
// handleOutput takes a word from stdout and returns a string to be written to stdin.
// See DetectUnamePass above for how this is used to check for a username/password request
// See RunAndDetectCredentialRequest above for how this is used to check for a username/password request
handleOutput func(string) string,
startCmd func(cmd *exec.Cmd) (*cmdHandler, error),
) error {
c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand")
cmdWriter := self.guiIO.newCmdWriterFn()
self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
if cmdObj.ShouldLog() {
c.LogCommand(cmdObj.ToString(), true)
self.logCmdObj(cmdObj)
}
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(writer, &stderr)
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
handler, err := startCmd(cmd)
if err != nil {
@ -81,11 +74,11 @@ func RunCommandWithOutputLiveAux(
defer func() {
if closeErr := handler.close(); closeErr != nil {
c.Log.Error(closeErr)
self.log.Error(closeErr)
}
}()
tr := io.TeeReader(handler.stdoutPipe, writer)
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
go utils.Safe(func() {
scanner := bufio.NewScanner(tr)

View file

@ -4,22 +4,19 @@
package oscommands
import (
"io"
"os/exec"
"github.com/creack/pty"
)
func RunCommandWithOutputLiveWrapper(
c *OSCommand,
// we define this separately for windows and non-windows given that windows does
// not have great PTY support and we need a PTY to handle a credential request
func (self *cmdObjRunner) RunCommandWithOutputLive(
cmdObj ICmdObj,
writer io.Writer,
output func(string) string,
) error {
return RunCommandWithOutputLiveAux(
c,
return self.RunCommandWithOutputLiveAux(
cmdObj,
writer,
output,
func(cmd *exec.Cmd) (*cmdHandler, error) {
ptmx, err := pty.Start(cmd)

View file

@ -26,18 +26,14 @@ func (b *Buffer) Write(p []byte) (n int, err error) {
return b.b.Write(p)
}
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
// RunCommandWithOutputLive runs a command live but because of windows compatibility this command can't be ran there
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
func RunCommandWithOutputLiveWrapper(
c *OSCommand,
func (self *cmdObjRunner) RunCommandWithOutputLive(
cmdObj ICmdObj,
writer io.Writer,
output func(string) string,
) error {
return RunCommandWithOutputLiveAux(
c,
return self.RunCommandWithOutputLiveAux(
cmdObj,
writer,
output,
func(cmd *exec.Cmd) (*cmdHandler, error) {
stdoutReader, stdoutWriter := io.Pipe()

View file

@ -0,0 +1,49 @@
package oscommands
import (
"io"
"io/ioutil"
"github.com/sirupsen/logrus"
)
// this struct captures some IO stuff
type guiIO struct {
// this is for logging anything we want. It'll be written to a log file for the sake
// of debugging.
log *logrus.Entry
// this is for us to log the command we're about to run e.g. 'git push'. The GUI
// will write this to a log panel so that the user can see which commands are being
// run.
// The isCommandLineCommand arg is there so that we can style the log differently
// depending on whether we're directly outputting a command we're about to run that
// will be run on the command line, or if we're using something from Go's standard lib.
logCommandFn func(str string, isCommandLineCommand bool)
// this is for us to directly write the output of a command. We will do this for
// certain commands like 'git push'. The GUI will write this to a command output panel.
// We need a new cmd writer per command, hence it being a function.
newCmdWriterFn func() io.Writer
// this allows us to request info from the user like username/password, in the event
// that a command requests it.
// the 'credential' arg is something like 'username' or 'password'
promptForCredentialFn func(credential string) string
}
func NewGuiIO(log *logrus.Entry, logCommandFn func(string, bool), newCmdWriterFn func() io.Writer, promptForCredentialFn func(string) string) *guiIO {
return &guiIO{
log: log,
logCommandFn: logCommandFn,
newCmdWriterFn: newCmdWriterFn,
promptForCredentialFn: promptForCredentialFn,
}
}
func NewNullGuiIO(log *logrus.Entry) *guiIO {
return &guiIO{
log: log,
logCommandFn: func(string, bool) {},
newCmdWriterFn: func() io.Writer { return ioutil.Discard },
promptForCredentialFn: func(string) string { return "" },
}
}

View file

@ -20,7 +20,7 @@ import (
type OSCommand struct {
*common.Common
Platform *Platform
Getenv func(string) string
GetenvFn func(string) string
// callback to run before running a command, i.e. for the purposes of logging.
// the string argument is the command string e.g. 'git add .' and the bool is
@ -43,24 +43,20 @@ type Platform struct {
}
// NewOSCommand os command runner
func NewOSCommand(common *common.Common, platform *Platform) *OSCommand {
func NewOSCommand(common *common.Common, platform *Platform, guiIO *guiIO) *OSCommand {
c := &OSCommand{
Common: common,
Platform: platform,
Getenv: os.Getenv,
GetenvFn: os.Getenv,
removeFile: os.RemoveAll,
}
runner := &cmdObjRunner{log: common.Log, logCmdObj: c.LogCmdObj}
runner := &cmdObjRunner{log: common.Log, guiIO: guiIO}
c.Cmd = &CmdObjBuilder{runner: runner, platform: platform}
return c
}
func (c *OSCommand) LogCmdObj(cmdObj ICmdObj) {
c.LogCommand(cmdObj.ToString(), true)
}
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
c.Log.WithField("command", cmdStr).Info("RunCommand")
@ -270,6 +266,10 @@ func (c *OSCommand) RemoveFile(path string) error {
return c.removeFile(path)
}
func (c *OSCommand) Getenv(key string) string {
return c.GetenvFn(key)
}
func GetTempDir() string {
return filepath.Join(os.TempDir(), "lazygit")
}