mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-08-09 09:22:48 +03:00
This check is not realy needed because the change that it will show up a second time is so low that this is more work to check than the change it actualy might happen
242 lines
6.7 KiB
Go
242 lines
6.7 KiB
Go
package commands
|
|
|
|
import (
|
|
"errors"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/mgutz/str"
|
|
"github.com/sirupsen/logrus"
|
|
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
|
|
}
|
|
|
|
// OSCommand holds all the os commands
|
|
type OSCommand struct {
|
|
Log *logrus.Entry
|
|
Platform *Platform
|
|
Config config.AppConfigurer
|
|
command func(string, ...string) *exec.Cmd
|
|
getGlobalGitConfig func(string) (string, error)
|
|
getenv func(string) string
|
|
}
|
|
|
|
// NewOSCommand os command runner
|
|
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
|
|
return &OSCommand{
|
|
Log: log,
|
|
Platform: getPlatform(),
|
|
Config: config,
|
|
command: exec.Command,
|
|
getGlobalGitConfig: gitconfig.Global,
|
|
getenv: os.Getenv,
|
|
}
|
|
}
|
|
|
|
// RunCommandWithOutput wrapper around commands returning their output and error
|
|
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
|
|
c.Log.WithField("command", command).Info("RunCommand")
|
|
splitCmd := str.ToArgv(command)
|
|
c.Log.Info(splitCmd)
|
|
return sanitisedCommandOutput(
|
|
c.command(splitCmd[0], splitCmd[1:]...).CombinedOutput(),
|
|
)
|
|
}
|
|
|
|
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
|
|
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) (errorMessage string, err error) {
|
|
return RunCommandWithOutputLiveWrapper(c, command, output)
|
|
}
|
|
|
|
// 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 := ""
|
|
errMessage, err := c.RunCommandWithOutputLive(command, func(word string) string {
|
|
ttyText = ttyText + " " + word
|
|
|
|
prompts := map[string]string{
|
|
"password": `Password\s*for\s*'.+':`,
|
|
"username": `Username\s*for\s*'.+':`,
|
|
}
|
|
|
|
for askFor, pattern := range prompts {
|
|
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
|
ttyText = ""
|
|
return ask(askFor)
|
|
}
|
|
}
|
|
|
|
return ""
|
|
})
|
|
if err != nil {
|
|
if strings.Contains("exit status 128", err.Error()) {
|
|
errMessage = "exit status 128"
|
|
}
|
|
return errors.New(errMessage)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RunCommand runs a command and just returns the error
|
|
func (c *OSCommand) RunCommand(command string) error {
|
|
_, err := c.RunCommandWithOutput(command)
|
|
return err
|
|
}
|
|
|
|
// 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"
|
|
}
|
|
|
|
// RunDirectCommand wrapper around direct commands
|
|
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
|
|
c.Log.WithField("command", command).Info("RunDirectCommand")
|
|
|
|
return sanitisedCommandOutput(
|
|
c.command(c.Platform.shell, c.Platform.shellArg, command).
|
|
CombinedOutput(),
|
|
)
|
|
}
|
|
|
|
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
|
|
if outputString == "" {
|
|
return "", err
|
|
}
|
|
return outputString, errors.New(outputString)
|
|
}
|
|
return outputString, nil
|
|
}
|
|
|
|
// OpenFile opens a file with the given
|
|
func (c *OSCommand) OpenFile(filename string) error {
|
|
commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand")
|
|
templateValues := map[string]string{
|
|
"filename": c.Quote(filename),
|
|
}
|
|
|
|
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
|
err := c.RunCommand(command)
|
|
return err
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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) {
|
|
editor, _ := c.getGlobalGitConfig("core.editor")
|
|
|
|
if editor == "" {
|
|
editor = c.getenv("VISUAL")
|
|
}
|
|
if editor == "" {
|
|
editor = c.getenv("EDITOR")
|
|
}
|
|
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")
|
|
}
|
|
|
|
return c.PrepareSubProcess(editor, filename), nil
|
|
}
|
|
|
|
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
|
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
|
|
return c.command(cmdName, commandArgs...)
|
|
}
|
|
|
|
// Quote wraps a message in platform-specific quotation marks
|
|
func (c *OSCommand) Quote(message string) string {
|
|
message = strings.Replace(message, "`", "\\`", -1)
|
|
escapedQuote := c.Platform.escapedQuote
|
|
if strings.Contains(message, c.Platform.escapedQuote) {
|
|
escapedQuote = c.Platform.fallbackEscapedQuote
|
|
}
|
|
return escapedQuote + message + escapedQuote
|
|
}
|
|
|
|
// 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 {
|
|
return strings.Replace(message, `"`, "", -1)
|
|
}
|
|
|
|
// AppendLineToFile adds a new line in file
|
|
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
|
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = f.WriteString("\n" + line)
|
|
return err
|
|
}
|
|
|
|
// 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 "", err
|
|
}
|
|
|
|
if _, err := tmpfile.Write([]byte(content)); err != nil {
|
|
c.Log.Error(err)
|
|
return "", err
|
|
}
|
|
if err := tmpfile.Close(); err != nil {
|
|
c.Log.Error(err)
|
|
return "", err
|
|
}
|
|
|
|
return tmpfile.Name(), nil
|
|
}
|
|
|
|
// RemoveFile removes a file at the specified path
|
|
func (c *OSCommand) RemoveFile(filename string) error {
|
|
return os.Remove(filename)
|
|
}
|