1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-12 11:15:00 +02:00
lazygit/pkg/commands/os.go

457 lines
12 KiB
Go
Raw Normal View History

2018-08-14 11:05:26 +02:00
package commands
import (
"bufio"
"fmt"
2018-12-05 10:33:46 +02:00
"io/ioutil"
2018-08-14 11:05:26 +02:00
"os"
"os/exec"
"path/filepath"
2018-10-17 20:38:13 +02:00
"regexp"
2018-08-16 23:04:39 +02:00
"strings"
"sync"
2018-08-14 11:05:26 +02:00
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
2018-08-14 11:05:26 +02:00
"github.com/mgutz/str"
2018-08-23 14:22:03 +02:00
"github.com/sirupsen/logrus"
2018-08-14 11:05:26 +02:00
gitconfig "github.com/tcnksm/go-gitconfig"
)
// Platform stores the os state
type Platform struct {
os string
shell string
shellArg string
escapedQuote string
openCommand string
openLinkCommand string
fallbackEscapedQuote string
2018-08-14 11:05:26 +02:00
}
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Entry
2018-08-22 22:31:35 +02:00
Platform *Platform
Config config.AppConfigurer
2018-08-22 22:31:35 +02:00
command func(string, ...string) *exec.Cmd
beforeExecuteCmd func(*exec.Cmd)
2018-08-22 22:31:35 +02:00
getGlobalGitConfig func(string) (string, error)
getenv func(string) string
2018-08-14 11:05:26 +02:00
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
2018-08-21 21:09:53 +02:00
return &OSCommand{
2018-08-22 22:31:35 +02:00
Log: log,
Platform: getPlatform(),
Config: config,
2018-08-22 22:31:35 +02:00
command: exec.Command,
beforeExecuteCmd: func(*exec.Cmd) {},
2018-08-22 22:31:35 +02:00
getGlobalGitConfig: gitconfig.Global,
getenv: os.Getenv,
2018-08-14 11:05:26 +02:00
}
}
// SetCommand sets the command function used by the struct.
// To be used for testing only
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.command = cmd
}
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
c.beforeExecuteCmd = cmd
}
2020-03-21 09:27:20 +02:00
type RunCommandOptions struct {
EnvVars []string
}
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, options.EnvVars...)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
_, err := c.RunCommandWithOutputWithOptions(command, options)
return err
}
2018-08-14 11:05:26 +02:00
// RunCommandWithOutput wrapper around commands returning their output and error
2019-11-21 13:17:18 +02:00
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
// however there's a bizarre compiler error/warning when you pass in a formatString
// with a percent sign because it thinks it's supposed to be a formatString when
// in that case it's not. To get around that error you'll need to define the string
// in a variable and pass the variable into RunCommandWithOutput.
func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
command := formatString
2019-11-21 12:45:18 +02:00
if formatArgs != nil {
command = fmt.Sprintf(formatString, formatArgs...)
}
2018-08-14 11:05:26 +02:00
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
c.beforeExecuteCmd(cmd)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutable runs an executable file and returns an error if there was one
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
_, err := c.RunExecutableWithOutput(cmd)
return err
}
// ExecutableFromString takes a string like `git status` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
cmd := c.command(splitCmd[0], splitCmd[1:]...)
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
return cmd
2018-08-14 11:05:26 +02:00
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
2018-12-12 23:10:33 +02:00
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, command, output)
2018-10-17 20:38:13 +02:00
}
// DetectUnamePass detect a username / password question in a command
// ask is a function that gets executen when this function detect you need to fillin a password
// The ask argument will be "username" or "password" and expects the user's password or username back
func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) error {
ttyText := ""
2018-12-12 23:10:33 +02:00
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
2018-10-17 20:38:13 +02:00
ttyText = ttyText + " " + word
prompts := map[string]string{
"password": `Password\s*for\s*'.+':`,
"username": `Username\s*for\s*'.+':`,
2018-10-17 20:38:13 +02:00
}
for askFor, pattern := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
2018-11-10 09:46:42 +02:00
ttyText = ""
return ask(askFor)
2018-11-10 09:46:42 +02:00
}
2018-10-17 20:38:13 +02:00
}
2018-11-10 09:46:42 +02:00
2018-10-17 20:38:13 +02:00
return ""
})
2018-12-12 23:10:33 +02:00
return errMessage
2018-10-17 20:38:13 +02:00
}
2018-08-14 11:05:26 +02:00
// RunCommand runs a command and just returns the error
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
2018-08-14 11:05:26 +02:00
return err
}
2018-08-28 11:12:35 +02:00
// FileType tells us if the file is a file, directory or other
func (c *OSCommand) FileType(path string) string {
fileInfo, err := os.Stat(path)
if err != nil {
return "other"
}
if fileInfo.IsDir() {
return "directory"
}
return "file"
}
2018-08-14 11:05:26 +02:00
// RunDirectCommand wrapper around direct commands
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
c.Log.WithField("command", command).Info("RunDirectCommand")
2018-08-21 21:48:42 +02:00
return sanitisedCommandOutput(
2018-09-01 04:13:41 +02:00
c.command(c.Platform.shell, c.Platform.shellArg, command).
2018-08-21 21:48:42 +02:00
CombinedOutput(),
)
2018-08-14 11:05:26 +02:00
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
2018-09-01 05:27:58 +02:00
if outputString == "" {
return "", WrapError(err)
2018-09-01 05:27:58 +02:00
}
2018-08-14 11:05:26 +02:00
return outputString, errors.New(outputString)
}
return outputString, nil
}
// OpenFile opens a file with the given
2018-08-21 22:17:34 +02:00
func (c *OSCommand) OpenFile(filename string) error {
2018-09-01 06:33:01 +02:00
commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand")
templateValues := map[string]string{
"filename": c.Quote(filename),
2018-08-14 11:05:26 +02:00
}
2018-08-21 22:17:34 +02:00
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
2018-09-01 06:33:01 +02:00
err := c.RunCommand(command)
return err
2018-08-14 11:05:26 +02:00
}
2018-11-28 01:33:52 +02:00
// OpenLink opens a file with the given
func (c *OSCommand) OpenLink(link string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand")
templateValues := map[string]string{
"link": c.Quote(link),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunCommand(command)
return err
}
2018-08-14 11:05:26 +02:00
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
2018-08-22 22:31:35 +02:00
editor, _ := c.getGlobalGitConfig("core.editor")
2018-08-21 22:33:25 +02:00
2018-08-14 11:05:26 +02:00
if editor == "" {
2018-08-22 22:31:35 +02:00
editor = c.getenv("VISUAL")
2018-08-14 11:05:26 +02:00
}
if editor == "" {
2018-08-22 22:31:35 +02:00
editor = c.getenv("EDITOR")
2018-08-14 11:05:26 +02:00
}
if editor == "" {
if err := c.RunCommand("which vi"); err == nil {
editor = "vi"
}
}
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
2018-08-14 11:05:26 +02:00
}
2018-08-21 22:33:25 +02:00
return c.PrepareSubProcess(editor, filename), nil
2018-08-14 11:05:26 +02:00
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
2018-08-21 22:33:25 +02:00
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
cmd := c.command(cmdName, commandArgs...)
if cmd != nil {
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
}
return cmd
2018-08-14 11:05:26 +02:00
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
2018-08-16 23:04:39 +02:00
message = strings.Replace(message, "`", "\\`", -1)
escapedQuote := c.Platform.escapedQuote
if strings.Contains(message, c.Platform.escapedQuote) {
escapedQuote = c.Platform.fallbackEscapedQuote
}
return escapedQuote + message + escapedQuote
2018-08-14 11:05:26 +02:00
}
2018-08-18 12:14:44 +02:00
// Unquote removes wrapping quotations marks if they are present
// this is needed for removing quotes from staged filenames with spaces
func (c *OSCommand) Unquote(message string) string {
2018-08-21 22:05:05 +02:00
return strings.Replace(message, `"`, "", -1)
2018-08-18 12:14:44 +02:00
}
2018-08-19 12:41:04 +02:00
2018-08-21 08:40:51 +02:00
// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
2018-08-19 12:41:04 +02:00
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return WrapError(err)
2018-08-19 12:41:04 +02:00
}
defer f.Close()
_, err = f.WriteString("\n" + line)
2019-02-20 11:01:29 +02:00
if err != nil {
return WrapError(err)
2019-02-20 11:01:29 +02:00
}
return nil
2018-08-19 12:41:04 +02:00
}
2018-12-05 10:33:46 +02:00
// CreateTempFile writes a string to a new temp file and returns the file's name
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := ioutil.TempFile("", filename)
if err != nil {
c.Log.Error(err)
return "", WrapError(err)
2018-12-05 10:33:46 +02:00
}
if _, err := tmpfile.WriteString(content); err != nil {
2018-12-05 10:33:46 +02:00
c.Log.Error(err)
return "", WrapError(err)
2018-12-05 10:33:46 +02:00
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", WrapError(err)
2018-12-05 10:33:46 +02:00
}
return tmpfile.Name(), nil
}
// CreateFileWithContent creates a file with the given content
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
c.Log.Error(err)
return err
}
if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
c.Log.Error(err)
return WrapError(err)
}
return nil
}
2019-03-18 11:44:33 +02:00
// Remove removes a file or directory at the specified path
func (c *OSCommand) Remove(filename string) error {
err := os.RemoveAll(filename)
return WrapError(err)
2018-12-05 10:33:46 +02:00
}
// FileExists checks whether a file exists at the specified path
func (c *OSCommand) FileExists(path string) (bool, error) {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
c.beforeExecuteCmd(cmd)
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
if err != nil {
if len(outString) == 0 {
return err
}
return errors.New(outString)
}
return nil
}
// GetLazygitPath returns the path of the currently executed file
func (c *OSCommand) GetLazygitPath() string {
ex, err := os.Executable() // get the executable path for git to use
if err != nil {
ex = os.Args[0] // fallback to the first call argument if needed
}
2020-01-07 20:25:00 +02:00
return `"` + filepath.ToSlash(ex) + `"`
}
2019-03-12 12:43:56 +02:00
// RunCustomCommand returns the pointer to a custom command
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
}
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
cmds := make([]*exec.Cmd, len(commandStrings))
for i, str := range commandStrings {
cmds[i] = c.ExecutableFromString(str)
}
for i := 0; i < len(cmds)-1; i++ {
stdout, err := cmds[i].StdoutPipe()
if err != nil {
return err
}
cmds[i+1].Stdin = stdout
}
// keeping this here in case I adapt this code for some other purpose in the future
// cmds[len(cmds)-1].Stdout = os.Stdout
finalErrors := []string{}
wg := sync.WaitGroup{}
wg.Add(len(cmds))
for _, cmd := range cmds {
currentCmd := cmd
go func() {
stderr, err := currentCmd.StderrPipe()
if err != nil {
c.Log.Error(err)
}
if err := currentCmd.Start(); err != nil {
c.Log.Error(err)
}
if b, err := ioutil.ReadAll(stderr); err == nil {
if len(b) > 0 {
finalErrors = append(finalErrors, string(b))
}
}
if err := currentCmd.Wait(); err != nil {
c.Log.Error(err)
}
wg.Done()
}()
}
wg.Wait()
if len(finalErrors) > 0 {
return errors.New(strings.Join(finalErrors, "\n"))
}
return nil
}
2020-03-01 03:30:48 +02:00
func Kill(cmd *exec.Cmd) error {
if cmd.Process == nil {
// somebody got to it before we were able to, poor bastard
return nil
}
return cmd.Process.Kill()
}
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
cmd.Process.Kill()
break
}
}
cmd.Wait()
return nil
}