diff --git a/Gopkg.lock b/Gopkg.lock index 4b84d6537..432138f18 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -189,11 +189,19 @@ [[projects]] branch = "master" - digest = "1:66bb9b4a5abb704642fccba52a84a7f7feef2d9623f87b700e52a6695044723f" + digest = "1:9b266d7748a5d94985fd9e323494f5b8ae1ab3e910418e898dfe7f03339ddbcd" name = "github.com/jesseduffield/gocui" packages = ["."] pruneopts = "NUT" - revision = "03e26ff3f1de2c1bc2205113c3aba661312eee00" + revision = "cfa9e452ba5ebf014041846851152d64a59dce14" + +[[projects]] + branch = "master" + digest = "1:a46c2f4863e5284ddb255c28750298e04bc8c0fc896bed6056e947673168b7be" + name = "github.com/jesseduffield/pty" + packages = ["."] + pruneopts = "NUT" + revision = "02db52c7e406c7abec44c717a173c7715e4c1b62" [[projects]] branch = "master" @@ -616,6 +624,7 @@ "github.com/heroku/rollrus", "github.com/jesseduffield/go-getter", "github.com/jesseduffield/gocui", + "github.com/jesseduffield/pty", "github.com/kardianos/osext", "github.com/mgutz/str", "github.com/nicksnyder/go-i18n/v2/i18n", diff --git a/Gopkg.toml b/Gopkg.toml index 1f0b03cee..a645c905f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -37,6 +37,10 @@ branch = "master" name = "github.com/jesseduffield/gocui" +[[constraint]] + branch = "master" + name = "github.com/jesseduffield/pty" + [[constraint]] name = "gopkg.in/src-d/go-git.v4" revision = "43d17e14b714665ab5bc2ecc220b6740779d733f" diff --git a/go.mod b/go.mod index bfa4f7cf4..c01fd368f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,8 @@ require ( github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63 - github.com/jesseduffield/gocui v0.0.0-20180921065632-03e26ff3f1de + github.com/jesseduffield/gocui v0.0.0-20190115084758-cfa9e452ba5e + github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406 github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 diff --git a/go.sum b/go.sum index 1506becb9..12cbeb220 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63 h1:Nrr/yUxNjXWYK0B3IqcFlYh1ICnesJDB4ogcfOVc5Ns= github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63/go.mod h1:fNqjRf+4XnTo2PrGN1JRb79b/BeoHwP4lU00f39SQY0= -github.com/jesseduffield/gocui v0.0.0-20180919095827-4fca348422d8 h1:XxX+IqNOFDh1PnU4eZDzUomoKbuKCvwyEm5an/IxLQU= -github.com/jesseduffield/gocui v0.0.0-20180919095827-4fca348422d8/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw= +github.com/jesseduffield/gocui v0.0.0-20181209104758-fe55a32c8a4c h1:jEfh/vAtfF3pQ8xFhpYR/0S4iHo11VfaYelJmzZJm94= +github.com/jesseduffield/gocui v0.0.0-20181209104758-fe55a32c8a4c/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw= +github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406 h1:iYMH6h6SuWuBkIzRtymosE8NpSgTK0oRMfyTdVWgxzc= +github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo= github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb h1:cFHYEWpQEfzFZVKiKZytCUX4UwQixKSw0kd3WhluPsY= github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= @@ -61,8 +63,8 @@ github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 h1:KXZJFdun github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80 h1:7ory6RlsEkeK89iyV7Imz3sVz8YHeSw29w3PehpCWC0= github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4= -github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5 h1:/TjjTS4kg7vC+05gD0LE4+97f/+PRFICnK/7wJPk7kE= -github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU= +github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.6 h1:SooTCzUOOs899x/M7gmSS+dgL+AUfSWqAcHLN3auCic= +github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.6/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU= github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= diff --git a/pkg/commands/exec_live_default.go b/pkg/commands/exec_live_default.go new file mode 100644 index 000000000..dfdff5308 --- /dev/null +++ b/pkg/commands/exec_live_default.go @@ -0,0 +1,100 @@ +// +build !windows + +package commands + +import ( + "bufio" + "bytes" + "errors" + "os" + "os/exec" + "strings" + "unicode/utf8" + + "github.com/jesseduffield/pty" + "github.com/mgutz/str" +) + +// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout +// 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 written anything to stdin +func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error { + splitCmd := str.ToArgv(command) + cmd := exec.Command(splitCmd[0], splitCmd[1:]...) + + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8") + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + ptmx, err := pty.Start(cmd) + + if err != nil { + return err + } + + go func() { + scanner := bufio.NewScanner(ptmx) + scanner.Split(scanWordsWithNewLines) + for scanner.Scan() { + toOutput := strings.Trim(scanner.Text(), " ") + _, _ = ptmx.WriteString(output(toOutput)) + } + }() + + err = cmd.Wait() + ptmx.Close() + if err != nil { + return errors.New(stderr.String()) + } + + return nil +} + +// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines +// For specific comments about this function take a look at: bufio.ScanWords +func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) { + start := 0 + for width := 0; start < len(data); start += width { + var r rune + r, width = utf8.DecodeRune(data[start:]) + if !isSpace(r) { + break + } + } + for width, i := 0, start; i < len(data); i += width { + var r rune + r, width = utf8.DecodeRune(data[i:]) + if isSpace(r) { + return i + width, data[start:i], nil + } + } + if atEOF && len(data) > start { + return len(data), data[start:], nil + } + return start, nil, nil +} + +// isSpace is also copied from the bufio package and has been modified to also captures new lines +// For specific comments about this function take a look at: bufio.isSpace +func isSpace(r rune) bool { + if r <= '\u00FF' { + switch r { + case ' ', '\t', '\v', '\f': + return true + case '\u0085', '\u00A0': + return true + } + return false + } + if '\u2000' <= r && r <= '\u200a' { + return true + } + switch r { + case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000': + return true + } + return false +} diff --git a/pkg/commands/exec_live_win.go b/pkg/commands/exec_live_win.go new file mode 100644 index 000000000..d06cb920b --- /dev/null +++ b/pkg/commands/exec_live_win.go @@ -0,0 +1,9 @@ +// +build windows + +package commands + +// RunCommandWithOutputLiveWrapper 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 +func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error { + return c.RunCommand(command) +} diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 7a0932501..4858aa85c 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -294,8 +294,13 @@ func (c *GitCommand) AbortMergeBranch() error { } // Fetch fetch git repo -func (c *GitCommand) Fetch() error { - return c.OSCommand.RunCommand("git fetch") +func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error { + return c.OSCommand.DetectUnamePass("git fetch", func(question string) string { + if canAskForCredentials { + return unamePassQuestion(question) + } + return "\n" + }) } // ResetToCommit reset to commit @@ -373,18 +378,19 @@ func (c *GitCommand) Commit(message string, amend bool) (*exec.Cmd, error) { } // Pull pulls from repo -func (c *GitCommand) Pull() error { - return c.OSCommand.RunCommand("git pull --no-edit") +func (c *GitCommand) Pull(ask func(string) string) error { + return c.OSCommand.DetectUnamePass("git pull --no-edit", ask) } // Push pushes to a branch -func (c *GitCommand) Push(branchName string, force bool) error { +func (c *GitCommand) Push(branchName string, force bool, ask func(string) string) error { forceFlag := "" if force { forceFlag = "--force-with-lease " } - return c.OSCommand.RunCommand(fmt.Sprintf("git push %s -u origin %s", forceFlag, branchName)) + cmd := fmt.Sprintf("git push %s -u origin %s", forceFlag, branchName) + return c.OSCommand.DetectUnamePass(cmd, ask) } // SquashPreviousTwoCommits squashes a commit down to the one below it diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index 997054bca..3dcb7798d 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -95,7 +95,7 @@ func TestVerifyInGitRepo(t *testing.T) { }, func(err error) { assert.Error(t, err) - assert.Regexp(t, "fatal: .ot a git repository \\(or any of the parent directories\\): \\.git", err.Error()) + assert.Regexp(t, `fatal: .ot a git repository \(or any of the parent directories\s?\/?\): \.git`, err.Error()) }, }, } @@ -256,7 +256,7 @@ func TestNewGitCommand(t *testing.T) { }, func(gitCmd *GitCommand, err error) { assert.Error(t, err) - assert.Regexp(t, "fatal: .ot a git repository \\(or any of the parent directories\\): \\.git", err.Error()) + assert.Regexp(t, `fatal: .ot a git repository ((\(or any of the parent directories\): \.git)|(\(or any parent up to mount point \/\)))`, err.Error()) }, }, { @@ -1010,7 +1010,7 @@ func TestGitCommandPush(t *testing.T) { }, false, func(err error) { - assert.Nil(t, err) + assert.Contains(t, err.Error(), "error: failed to push some refs") }, }, { @@ -1023,7 +1023,7 @@ func TestGitCommandPush(t *testing.T) { }, true, func(err error) { - assert.Nil(t, err) + assert.Contains(t, err.Error(), "error: failed to push some refs") }, }, { @@ -1031,12 +1031,11 @@ func TestGitCommandPush(t *testing.T) { func(cmd string, args ...string) *exec.Cmd { assert.EqualValues(t, "git", cmd) assert.EqualValues(t, []string{"push", "-u", "origin", "test"}, args) - return exec.Command("test") }, false, func(err error) { - assert.Error(t, err) + assert.Contains(t, err.Error(), "error: failed to push some refs") }, }, } @@ -1045,7 +1044,10 @@ func TestGitCommandPush(t *testing.T) { t.Run(s.testName, func(t *testing.T) { gitCmd := newDummyGitCommand() gitCmd.OSCommand.command = s.command - s.test(gitCmd.Push("test", s.forcePush)) + err := gitCmd.Push("test", s.forcePush, func(passOrUname string) string { + return "\n" + }) + s.test(err) }) } } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 829034eb8..186887b5d 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "os/exec" + "regexp" "strings" "github.com/jesseduffield/lazygit/pkg/config" @@ -57,6 +58,36 @@ func (c *OSCommand) RunCommandWithOutput(command string) (string, error) { ) } +// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper +func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) 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 := 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 "" + }) + return errMessage +} + // RunCommand runs a command and just returns the error func (c *OSCommand) RunCommand(command string) error { _, err := c.RunCommandWithOutput(command) @@ -186,7 +217,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { return "", err } - if _, err := tmpfile.Write([]byte(content)); err != nil { + if _, err := tmpfile.WriteString(content); err != nil { c.Log.Error(err) return "", err } diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index f789349b4..18aa961bc 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -20,6 +20,7 @@ type AppConfig struct { BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""` UserConfig *viper.Viper AppState *AppState + IsNewRepo bool } // AppConfigurer interface allows individual app config structs to inherit Fields @@ -36,6 +37,8 @@ type AppConfigurer interface { WriteToUserConfig(string, string) error SaveAppState() error LoadAppState() error + SetIsNewRepo(bool) + GetIsNewRepo() bool } // NewAppConfig makes a new app config @@ -54,6 +57,7 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg BuildSource: buildSource, UserConfig: userConfig, AppState: &AppState{}, + IsNewRepo: false, } if err := appConfig.LoadAppState(); err != nil { @@ -63,6 +67,16 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg return appConfig, nil } +// GetIsNewRepo returns known repo boolean +func (c *AppConfig) GetIsNewRepo() bool { + return c.IsNewRepo +} + +// SetIsNewRepo set if the current repo is known +func (c *AppConfig) SetIsNewRepo(toSet bool) { + c.IsNewRepo = toSet +} + // GetDebug returns debug flag func (c *AppConfig) GetDebug() bool { return c.Debug @@ -153,7 +167,7 @@ func prepareConfigFile(filename string) (string, error) { } // LoadAndMergeFile Loads the config/state file, creating -// the file as an empty one if it does not exist +// the file has an empty one if it does not exist func LoadAndMergeFile(v *viper.Viper, filename string) error { configPath, err := prepareConfigFile(filename) if err != nil { @@ -242,9 +256,9 @@ type AppState struct { func getDefaultAppState() []byte { return []byte(` - lastUpdateCheck: 0 - recentRepos: [] -`) + lastUpdateCheck: 0 + recentRepos: [] + `) } // // commenting this out until we use it again diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index b94c91e28..54fd828fd 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -161,6 +161,17 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error return nil } +func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error { + if err := gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("FetchWait")); err != nil { + return err + } + go func() { + unamePassOpend, err := gui.fetch(g, v, true) + gui.HandleCredentialsPopup(g, unamePassOpend, err) + }() + return nil +} + func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error { branch := gui.getSelectedBranch() message := gui.Tr.SLocalize("SureForceCheckout") @@ -223,14 +234,14 @@ func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error { func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error { title := gui.Tr.SLocalize("DeleteBranch") - var messageId string + var messageID string if force { - messageId = "ForceDeleteBranchMessage" + messageID = "ForceDeleteBranchMessage" } else { - messageId = "DeleteBranchMessage" + messageID = "DeleteBranchMessage" } message := gui.Tr.TemplateLocalize( - messageId, + messageID, Teml{ "selectedBranchName": selectedBranch.Name, }, @@ -240,9 +251,8 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c errMessage := err.Error() if !force && strings.Contains(errMessage, "is not fully merged") { return gui.deleteNamedBranch(g, v, selectedBranch, true) - } else { - return gui.createErrorPanel(g, errMessage) } + return gui.createErrorPanel(g, errMessage) } return gui.refreshSidePanels(g) }, nil) diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 66e859be3..b2a202ddd 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -37,19 +37,24 @@ func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error { return g.DeleteView("confirmation") } -func (gui *Gui) getMessageHeight(message string, width int) int { +func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int { lines := strings.Split(message, "\n") lineCount := 0 - for _, line := range lines { - lineCount += len(line)/width + 1 + // if we need to wrap, calculate height to fit content within view's width + if wrap { + for _, line := range lines { + lineCount += len(line)/width + 1 + } + } else { + lineCount = len(lines) } return lineCount } -func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) { +func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) { width, height := g.Size() panelWidth := width / 2 - panelHeight := gui.getMessageHeight(prompt, panelWidth) + panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) return width/2 - panelWidth/2, height/2 - panelHeight/2 - panelHeight%2 - 1, width/2 + panelWidth/2, @@ -67,7 +72,7 @@ func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title s } func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string) (*gocui.View, error) { - x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, prompt) + x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt) confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) if err != nil { if err != gocui.ErrUnknownView { @@ -84,10 +89,15 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt } func (gui *Gui) onNewPopupPanel() { - gui.g.SetViewOnBottom("commitMessage") - gui.g.SetViewOnBottom("menu") + viewNames = []string{"commitMessage", + "credentials", + "menu"} + for _, viewName := range viewNames { + _, _ = gui.g.SetViewOnBottom(viewName) + } } +// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() g.Update(func(g *gocui.Gui) error { @@ -137,18 +147,27 @@ func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, return gui.createConfirmationPanel(g, currentView, title, prompt, nil, nil) } -func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error { - go func() { - // when reporting is switched on this log call sometimes introduces - // a delay on the error panel popping up. Here I'm adding a second wait - // so that the error is logged while the user is reading the error message - time.Sleep(time.Second) - gui.Log.Error(message) - }() +// createSpecificErrorPanel allows you to create an error popup, specifying the +// view to be focused when the user closes the popup, and a boolean specifying +// whether we will log the error. If the message may include a user password, +// this function is to be used over the more generic createErrorPanel, with +// willLog set to false +func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, willLog bool) error { + if willLog { + go func() { + // when reporting is switched on this log call sometimes introduces + // a delay on the error panel popping up. Here I'm adding a second wait + // so that the error is logged while the user is reading the error message + time.Sleep(time.Second) + gui.Log.Error(message) + }() + } - // gui.Log.WithField("staging", "staging").Info("creating confirmation panel") - currentView := g.CurrentView() colorFunction := color.New(color.FgRed).SprintFunc() coloredMessage := colorFunction(strings.TrimSpace(message)) - return gui.createConfirmationPanel(g, currentView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil) + return gui.createConfirmationPanel(gui.g, nextView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil) +} + +func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error { + return gui.createSpecificErrorPanel(message, g.CurrentView(), true) } diff --git a/pkg/gui/credentials_panel.go b/pkg/gui/credentials_panel.go new file mode 100644 index 000000000..bc894c422 --- /dev/null +++ b/pkg/gui/credentials_panel.go @@ -0,0 +1,104 @@ +package gui + +import ( + "strings" + + "github.com/jesseduffield/gocui" +) + +type credentials chan string + +// waitForPassUname wait for a username or password input from the credentials popup +func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUname string) string { + gui.credentials = make(chan string) + g.Update(func(g *gocui.Gui) error { + credentialsView, _ := g.View("credentials") + if passOrUname == "username" { + credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername") + credentialsView.Mask = 0 + } else { + credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword") + credentialsView.Mask = '*' + } + err := gui.switchFocus(g, currentView, credentialsView) + if err != nil { + return err + } + gui.RenderCommitLength() + return nil + }) + + // wait for username/passwords input + userInput := <-gui.credentials + return userInput + "\n" +} + +func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error { + message := gui.trimmedContent(v) + gui.credentials <- message + err := gui.refreshFiles(g) + if err != nil { + return err + } + v.Clear() + err = v.SetCursor(0, 0) + if err != nil { + return err + } + _, err = g.SetViewOnBottom("credentials") + if err != nil { + return err + } + nextView, err := gui.g.View("confirmation") + if err != nil { + nextView = gui.getFilesView(g) + } + err = gui.switchFocus(g, nil, nextView) + if err != nil { + return err + } + return gui.refreshCommits(g) +} + +func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error { + _, err := g.SetViewOnBottom("credentials") + if err != nil { + return err + } + + gui.credentials <- "" + return gui.switchFocus(g, nil, gui.getFilesView(g)) +} + +func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error { + if _, err := g.SetViewOnTop("credentials"); err != nil { + return err + } + + message := gui.Tr.TemplateLocalize( + "CloseConfirm", + Teml{ + "keyBindClose": "esc", + "keyBindConfirm": "enter", + }, + ) + return gui.renderString(g, "options", message) +} + +// HandleCredentialsPopup handles the views after executing a command that might ask for credentials +func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr error) { + if popupOpened { + _, _ = gui.g.SetViewOnBottom("credentials") + } + if cmdErr != nil { + errMessage := cmdErr.Error() + if strings.Contains(errMessage, "Invalid username or password") { + errMessage = gui.Tr.SLocalize("PassUnameWrong") + } + // we are not logging this error because it may contain a password + _ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(gui.g), false) + } else { + _ = gui.closeConfirmationPrompt(g) + _ = gui.refreshSidePanels(g) + } +} diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 3c8e6c7e7..915ff45c3 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -26,7 +26,7 @@ func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) { return gui.State.Files[selectedLine], nil } -func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bool) error { file, err := gui.getSelectedFile(g) if err != nil { if err != gui.Errors.ErrNoFiles { @@ -48,10 +48,18 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error { } content := gui.GitCommand.Diff(file, false) + if alreadySelected { + g.Update(func(*gocui.Gui) error { + return gui.setViewContent(gui.g, gui.getMainView(gui.g), content) + }) + return nil + } return gui.renderString(g, "main", content) } func (gui *Gui) refreshFiles() error { + selectedFile, _ := gui.getSelectedFile(gui.g) + filesView := gui.getFilesView() gui.refreshStateFiles() @@ -64,8 +72,10 @@ func (gui *Gui) refreshFiles() error { } fmt.Fprint(filesView, list) - if filesView == gui.g.CurrentView() { - return gui.handleFileSelect(gui.g, filesView) + if filesView == g.CurrentView() { + newSelectedFile, _ := gui.getSelectedFile(gui.g) + alreadySelected := newSelectedFile.Name == selectedFile.Name + return gui.handleFileSelect(g, filesView, alreadySelected) } return nil }) @@ -77,20 +87,14 @@ func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error { panelState := gui.State.Panels.Files gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), false) - if err := gui.resetOrigin(gui.getMainView()); err != nil { - return err - } - return gui.handleFileSelect(gui.g, v) + return gui.handleFileSelect(gui.g, v, false) } func (gui *Gui) handleFilesPrevLine(g *gocui.Gui, v *gocui.View) error { panelState := gui.State.Panels.Files gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), true) - if err := gui.resetOrigin(gui.getMainView()); err != nil { - return err - } - return gui.handleFileSelect(gui.g, v) + return gui.handleFileSelect(gui.g, v, false) } // specific functions @@ -169,7 +173,7 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error { return err } - return gui.handleFileSelect(g, v) + return gui.handleFileSelect(g, v, true) } func (gui *Gui) allFilesStaged() bool { @@ -376,32 +380,32 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) { } func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error { - gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PullWait")) + if err := gui.createMessagePanel(gui.g, v, "", gui.Tr.SLocalize("PullWait")); err != nil { + return err + } go func() { - if err := gui.GitCommand.Pull(); err != nil { - gui.createErrorPanel(g, err.Error()) - } else { - gui.closeConfirmationPrompt(g) - gui.refreshCommits(g) - gui.refreshStatus(g) - } - gui.refreshFiles() + unamePassOpend := false + err := gui.GitCommand.Pull(func(passOrUname string) string { + unamePassOpend = true + return gui.waitForPassUname(g, v, passOrUname) + }) + gui.HandleCredentialsPopup(g, unamePassOpend, err) }() return nil } -func (gui *Gui) pushWithForceFlag(currentView *gocui.View, force bool) error { - if err := gui.createMessagePanel(gui.g, currentView, "", gui.Tr.SLocalize("PushWait")); err != nil { +func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error { + if err := gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PushWait")); err != nil { return err } go func() { + unamePassOpend := false branchName := gui.State.Branches[0].Name - if err := gui.GitCommand.Push(branchName, force); err != nil { - _ = gui.createErrorPanel(gui.g, err.Error()) - } else { - _ = gui.closeConfirmationPrompt(gui.g) - _ = gui.refreshSidePanels(gui.g) - } + err := gui.GitCommand.Push(branchName, force, func(passOrUname string) string { + unamePassOpend = true + return gui.waitForPassUname(g, v, passOrUname) + }) + gui.HandleCredentialsPopup(g, unamePassOpend, err) }() return nil } @@ -410,10 +414,10 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error { // if we have pullables we'll ask if the user wants to force push _, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount() if pullables == "?" || pullables == "0" { - return gui.pushWithForceFlag(v, false) + return gui.pushWithForceFlag(g, v, false) } err := gui.createConfirmationPanel(g, nil, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error { - return gui.pushWithForceFlag(v, true) + return gui.pushWithForceFlag(g, v, true) }, nil) return err } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 2b6eccb5d..c33718937 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -1,6 +1,7 @@ package gui import ( + "sync" // "io" // "io/ioutil" @@ -15,6 +16,7 @@ import ( // "strings" + "github.com/fatih/color" "github.com/golang-collections/collections/stack" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" @@ -70,6 +72,8 @@ type Gui struct { Errors SentinelErrors Updater *updates.Updater statusManager *statusManager + credentials credentials + waitForIntro sync.WaitGroup } // for now the staging panel state, unlike the other panel states, is going to be @@ -356,6 +360,23 @@ func (gui *Gui) layout(g *gocui.Gui) error { } } + if check, _ := g.View("credentials"); check == nil { + // doesn't matter where this view starts because it will be hidden + if credentialsView, err := g.SetView("credentials", 0, 0, width/2, height/2, 0); err != nil { + if err != gocui.ErrUnknownView { + return err + } + _, err := g.SetViewOnBottom("credentials") + if err != nil { + return err + } + credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername") + credentialsView.FgColor = gocui.ColorWhite + credentialsView.Editable = true + credentialsView.Editor = gocui.EditorFunc(gui.simpleEditor) + } + } + if appStatusView, err := g.SetView("appStatus", -1, optionsTop, width, optionsTop+2, 0); err != nil { if err != gocui.ErrUnknownView { return err @@ -385,6 +406,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { if err := gui.updateRecentRepoList(); err != nil { return err } + gui.waitForIntro.Done() if _, err := gui.g.SetCurrentView(filesView.Name()); err != nil { return err @@ -419,35 +441,54 @@ func (gui *Gui) layout(g *gocui.Gui) error { // here is a good place log some stuff // if you download humanlog and do tail -f development.log | humanlog // this will let you see these branches as prettified json - // gui.Log.Info(utils.AsJson(gui.State.Files)) - + // gui.Log.Info(utils.AsJson(gui.State.Branches[0:4])) return gui.resizeCurrentPopupPanel(g) } func (gui *Gui) promptAnonymousReporting() error { return gui.createConfirmationPanel(gui.g, nil, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error { + gui.waitForIntro.Done() return gui.Config.WriteToUserConfig("reporting", "on") }, func(g *gocui.Gui, v *gocui.View) error { + gui.waitForIntro.Done() return gui.Config.WriteToUserConfig("reporting", "off") }) } -func (gui *Gui) fetch() error { - gui.GitCommand.Fetch() - gui.refreshStatus(gui.g) - return nil +func (gui *Gui) fetch(g *gocui.Gui, v *gocui.View, canAskForCredentials bool) (unamePassOpend bool, err error) { + unamePassOpend = false + err = gui.GitCommand.Fetch(func(passOrUname string) string { + unamePassOpend = true + return gui.waitForPassUname(gui.g, v, passOrUname) + }, canAskForCredentials) + + if canAskForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") { + colorFunction := color.New(color.FgRed).SprintFunc() + coloredMessage := colorFunction(strings.TrimSpace(gui.Tr.SLocalize("PassUnameWrong"))) + close := func(g *gocui.Gui, v *gocui.View) error { + return nil + } + _ = gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Error"), coloredMessage, close, close) + } + + gui.refreshStatus(g) + return unamePassOpend, err } -func (gui *Gui) updateLoader() error { - if view, _ := gui.g.View("confirmation"); view != nil { - content := gui.trimmedContent(view) - if strings.Contains(content, "...") { - staticContent := strings.Split(content, "...")[0] + "..." - if err := gui.renderString(gui.g, "confirmation", staticContent+" "+utils.Loader()); err != nil { - return err +func (gui *Gui) updateLoader(g *gocui.Gui) error { + gui.g.Update(func(g *gocui.Gui) error { + if view, _ := g.View("confirmation"); view != nil { + content := gui.trimmedContent(view) + if strings.Contains(content, "...") { + staticContent := strings.Split(content, "...")[0] + "..." + if err := gui.setViewContent(g, view, staticContent+" "+utils.Loader()); err != nil { + return err + } } } - } + return nil + }) + return nil } @@ -490,10 +531,31 @@ func (gui *Gui) Run() error { return err } - gui.goEvery(time.Second*60, gui.fetch) - gui.goEvery(time.Second*2, gui.refreshFiles) - gui.goEvery(time.Millisecond*50, gui.updateLoader) - gui.goEvery(time.Millisecond*50, gui.renderAppStatus) + if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" { + gui.waitForIntro.Add(2) + } else { + gui.waitForIntro.Add(1) + } + + go func() { + gui.waitForIntro.Wait() + isNew := gui.Config.GetIsNewRepo() + if !isNew { + time.After(60 * time.Second) + } + _, err := gui.fetch(g, g.CurrentView(), false) + if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew { + _ = gui.createConfirmationPanel(g, g.CurrentView(), gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil) + } else { + gui.goEvery(g, time.Second*60, func(g *gocui.Gui) error { + _, err := gui.fetch(g, g.CurrentView(), false) + return err + }) + } + }() + gui.goEvery(g, time.Second*10, gui.refreshFiles) + gui.goEvery(g, time.Millisecond*50, gui.updateLoader) + gui.goEvery(g, time.Millisecond*50, gui.renderAppStatus) g.SetManagerFunc(gui.layout) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 410d4545c..c31d51ae9 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -12,7 +12,6 @@ type Binding struct { Handler func(*gocui.Gui, *gocui.View) error Key interface{} // FIXME: find out how to get `gocui.Key | rune` Modifier gocui.Modifier - KeyReadable string Description string } @@ -23,16 +22,26 @@ func (b *Binding) GetDisplayStrings() []string { // GetKey is a function. func (b *Binding) GetKey() string { - r, ok := b.Key.(rune) - key := "" + key := 0 - if ok { - key = string(r) - } else if b.KeyReadable != "" { - key = b.KeyReadable + switch b.Key.(type) { + case rune: + key = int(b.Key.(rune)) + case gocui.Key: + key = int(b.Key.(gocui.Key)) } - return key + // special keys + switch key { + case 27: + return "esc" + case 13: + return "enter" + case 32: + return "space" + } + + return string(key) } // GetKeybindings is a function. @@ -144,7 +153,6 @@ func (gui *Gui) GetKeybindings() []*Binding { Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress, - KeyReadable: "space", Description: gui.Tr.SLocalize("toggleStaged"), }, { ViewName: "files", @@ -212,7 +220,12 @@ func (gui *Gui) GetKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleEnterFile, Description: gui.Tr.SLocalize("StageLines"), - KeyReadable: "enter", + }, { + ViewName: "files", + Key: 'f', + Modifier: gocui.ModNone, + Handler: gui.handleGitFetch, + Description: gui.Tr.SLocalize("fetch"), }, { ViewName: "merging", Key: gocui.KeyEsc, @@ -282,7 +295,6 @@ func (gui *Gui) GetKeybindings() []*Binding { Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleBranchPress, - KeyReadable: "space", Description: gui.Tr.SLocalize("checkout"), }, { ViewName: "branches", @@ -367,7 +379,6 @@ func (gui *Gui) GetKeybindings() []*Binding { Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply, - KeyReadable: "space", Description: gui.Tr.SLocalize("apply"), }, { ViewName: "stash", @@ -391,6 +402,16 @@ func (gui *Gui) GetKeybindings() []*Binding { Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose, + }, { + ViewName: "credentials", + Key: gocui.KeyEnter, + Modifier: gocui.ModNone, + Handler: gui.handleSubmitCredential, + }, { + ViewName: "credentials", + Key: gocui.KeyEsc, + Modifier: gocui.ModNone, + Handler: gui.handleCloseCredentialsView, }, { ViewName: "menu", Key: gocui.KeyEsc, @@ -406,7 +427,6 @@ func (gui *Gui) GetKeybindings() []*Binding { Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleStagingEscape, - KeyReadable: "esc", Description: gui.Tr.SLocalize("EscapeStaging"), }, { ViewName: "staging", diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 6dca1ad35..4feb416ec 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -55,7 +55,7 @@ func (gui *Gui) createMenu(title string, items interface{}, handlePress func(int return err } - x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, list) + x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list) menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0) menuView.Title = title menuView.FgColor = gocui.ColorWhite diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go index 13c26ccd0..e14a917eb 100644 --- a/pkg/gui/recent_repos_panel.go +++ b/pkg/gui/recent_repos_panel.go @@ -14,7 +14,7 @@ type recentRepo struct { path string } -// GetDisplayStrings is a function. +// GetDisplayStrings returns the path from a recent repo. func (r *recentRepo) GetDisplayStrings() []string { yellow := color.New(color.FgMagenta) base := filepath.Base(r.path) @@ -55,16 +55,22 @@ func (gui *Gui) updateRecentRepoList() error { if err != nil { return err } - gui.Config.GetAppState().RecentRepos = newRecentReposList(recentRepos, currentRepo) + known, recentRepos := newRecentReposList(recentRepos, currentRepo) + gui.Config.SetIsNewRepo(known) + gui.Config.GetAppState().RecentRepos = recentRepos return gui.Config.SaveAppState() } -func newRecentReposList(recentRepos []string, currentRepo string) []string { +// newRecentReposList returns a new repo list with a new entry but only when it doesn't exist yet +func newRecentReposList(recentRepos []string, currentRepo string) (bool, []string) { + isNew := true newRepos := []string{currentRepo} for _, repo := range recentRepos { if repo != currentRepo { newRepos = append(newRepos, repo) + } else { + isNew = false } } - return newRepos + return isNew, newRepos } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index df32e8fb5..7d343c9a4 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -90,7 +90,7 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { case "status": return gui.handleStatusSelect(g, v) case "files": - return gui.handleFileSelect(g, v) + return gui.handleFileSelect(g, v, false) case "branches": return gui.handleBranchSelect(g, v) case "commits": @@ -101,11 +101,15 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { return nil case "commitMessage": return gui.handleCommitFocused(g, v) - case "merging": + case "credentials": + return gui.handleCredentialsViewFocused(g, v) + case "main": // TODO: pull this out into a 'view focused' function gui.refreshMergePanel(g) v.Highlight = false return nil + case "merging": + return nil case "staging": return nil // return gui.handleStagingSelect(g, v) @@ -238,19 +242,28 @@ func (gui *Gui) focusPoint(cx int, cy int, v *gocui.View) error { return nil } +func (gui *Gui) cleanString(s string) string { + output := string(bom.Clean([]byte(s))) + return utils.NormalizeLinefeeds(output) +} + +func (gui *Gui) setViewContent(g *gocui.Gui, v *gocui.View, s string) error { + v.Clear() + fmt.Fprint(v, gui.cleanString(s)) + return nil +} + +// renderString resets the origin of a view and sets its content func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error { g.Update(func(*gocui.Gui) error { v, err := g.View(viewName) - // just in case the view disappeared as this function was called, we'll - // silently return if it's not found if err != nil { - return nil + return nil // return gracefully if view has been deleted } - v.Clear() - output := string(bom.Clean([]byte(s))) - output = utils.NormalizeLinefeeds(output) - fmt.Fprint(v, output) - return nil + if err := v.SetOrigin(0, 0); err != nil { + return err + } + return gui.setViewContent(gui.g, v, s) }) return nil } @@ -321,7 +334,7 @@ func (gui *Gui) currentViewName(g *gocui.Gui) string { func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error { v := g.CurrentView() - if v.Name() == "commitMessage" || v.Name() == "confirmation" { + if v.Name() == "commitMessage" || v.Name() == "credentials" || v.Name() == "confirmation" { return gui.resizePopupPanel(g, v) } return nil @@ -331,7 +344,7 @@ func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error { // If the confirmation panel is already displayed, just resize the width, // otherwise continue content := v.Buffer() - x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content) + x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Wrap, content) vx0, vy0, vx1, vy1 := v.Dimensions() if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 { return nil diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 8119725f0..a05eb053e 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -31,6 +31,15 @@ func addDutch(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CommitMessage", Other: "Commit bericht", + }, &i18n.Message{ + ID: "CredentialsUsername", + Other: "Gebruikersnaam", + }, &i18n.Message{ + ID: "CredentialsPassword", + Other: "Wachtwoord", + }, &i18n.Message{ + ID: "PassUnameWrong", + Other: "Wachtwoord en/of gebruikersnaam verkeert", }, &i18n.Message{ ID: "CommitChanges", Other: "Commit veranderingen", @@ -129,10 +138,13 @@ func addDutch(i18nObject *i18n.Bundle) error { Other: "Dit is geen bestand", }, &i18n.Message{ ID: "PullWait", - Other: "Pulling...", + Other: "Pullen...", }, &i18n.Message{ ID: "PushWait", - Other: "Pushing...", + Other: "Pushen...", + }, &i18n.Message{ + ID: "FetchWait", + Other: "Fetchen...", }, &i18n.Message{ ID: "FileNoMergeCons", Other: "Dit bestand heeft geen merge conflicten", @@ -409,12 +421,21 @@ func addDutch(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "NoBranchOnRemote", Other: `Deze branch bestaat niet op de remote. U moet het eerst naar de remote pushen.`, + }, &i18n.Message{ + ID: "fetch", + Other: `fetch`, + }, &i18n.Message{ + ID: "NoAutomaticGitFetchTitle", + Other: `Geen automatiese git fetch`, + }, &i18n.Message{ + ID: "NoAutomaticGitFetchBody", + Other: `Lazygit kan niet "git fetch" uitvoeren in een privé repository, gebruik f in het branches paneel om "git fetch" manueel uit te voeren`, }, &i18n.Message{ ID: "StageLines", - Other: `stage individual hunks/lines`, + Other: `stage individuele hunks/lijnen`, }, &i18n.Message{ ID: "FileStagingRequirements", - Other: `Can only stage individual lines for tracked files with unstaged changes`, + Other: `Kan alleen individuele lijnen stagen van getrackte bestanden met onstaged veranderingen`, }, &i18n.Message{ ID: "StagingTitle", Other: `Staging`, @@ -423,16 +444,16 @@ func addDutch(i18nObject *i18n.Bundle) error { Other: `stage hunk`, }, &i18n.Message{ ID: "StageLine", - Other: `stage line`, + Other: `stage lijn`, }, &i18n.Message{ ID: "EscapeStaging", - Other: `return to files panel`, + Other: `ga terug naar het bestanden paneel`, }, &i18n.Message{ ID: "CantFindHunks", - Other: `Could not find any hunks in this patch`, + Other: `Kan geen hunks vinden in deze patch`, }, &i18n.Message{ ID: "CantFindHunk", - Other: `Could not find hunk`, + Other: `Kan geen hunk vinden`, }, ) } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 86a7b6fee..7ee1c147d 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -39,6 +39,15 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CommitMessage", Other: "Commit message", + }, &i18n.Message{ + ID: "CredentialsUsername", + Other: "Username", + }, &i18n.Message{ + ID: "CredentialsPassword", + Other: "Password", + }, &i18n.Message{ + ID: "PassUnameWrong", + Other: "Password and/or username wrong", }, &i18n.Message{ ID: "CommitChanges", Other: "commit changes", @@ -141,6 +150,9 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "PushWait", Other: "Pushing...", + }, &i18n.Message{ + ID: "FetchWait", + Other: "Fetching...", }, &i18n.Message{ ID: "FileNoMergeCons", Other: "This file has no merge conflicts", @@ -417,6 +429,15 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "NoBranchOnRemote", Other: `This branch doesn't exist on remote. You need to push it to remote first.`, + }, &i18n.Message{ + ID: "fetch", + Other: `fetch`, + }, &i18n.Message{ + ID: "NoAutomaticGitFetchTitle", + Other: `No automatic git fetch`, + }, &i18n.Message{ + ID: "NoAutomaticGitFetchBody", + Other: `Lazygit can't use "git fetch" in a private repo; use 'f' in the files panel to run "git fetch" manually`, }, &i18n.Message{ ID: "StageLines", Other: `stage individual hunks/lines`, diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 3bc875ce4..4e92cb55d 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -29,6 +29,15 @@ func addPolish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "CommitMessage", Other: "Wiadomość commita", + }, &i18n.Message{ + ID: "CredentialsUsername", + Other: "Username", + }, &i18n.Message{ + ID: "CredentialsPassword", + Other: "Password", + }, &i18n.Message{ + ID: "PassUnameWrong", + Other: "Password and/or username wrong", }, &i18n.Message{ ID: "CommitChanges", Other: "commituj zmiany", @@ -122,6 +131,9 @@ func addPolish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "PushWait", Other: "Wypychanie zmian...", + }, &i18n.Message{ + ID: "FetchWait", + Other: "Fetching...", }, &i18n.Message{ ID: "FileNoMergeCons", Other: "Ten plik nie powoduje konfliktów scalania", @@ -392,30 +404,39 @@ func addPolish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "NoBranchOnRemote", Other: `Ta gałąź nie istnieje na zdalnym. Najpierw musisz go odepchnąć na odległość.`, + }, &i18n.Message{ + ID: "fetch", + Other: `fetch`, + }, &i18n.Message{ + ID: "NoAutomaticGitFetchTitle", + Other: `No automatic git fetch`, + }, &i18n.Message{ + ID: "NoAutomaticGitFetchBody", + Other: `Lazygit can't use "git fetch" in a private repo use f in the branches panel to run "git fetch" manually`, }, &i18n.Message{ ID: "StageLines", - Other: `stage individual hunks/lines`, + Other: `zatwierdź pojedyncze linie`, }, &i18n.Message{ ID: "FileStagingRequirements", - Other: `Can only stage individual lines for tracked files with unstaged changes`, + Other: `Można tylko zatwierdzić pojedyncze linie dla śledzonych plików z niezatwierdzonymi zmianami`, }, &i18n.Message{ ID: "StagingTitle", - Other: `Staging`, + Other: `Zatwierdzanie`, }, &i18n.Message{ ID: "StageHunk", - Other: `stage hunk`, + Other: `zatwierdź kawałek`, }, &i18n.Message{ ID: "StageLine", - Other: `stage line`, + Other: `zatwierdź linię`, }, &i18n.Message{ ID: "EscapeStaging", - Other: `return to files panel`, + Other: `wróć do panelu plików`, }, &i18n.Message{ ID: "CantFindHunks", - Other: `Could not find any hunks in this patch`, + Other: `Nie można znaleźć żadnych kawałków w tej łatce`, }, &i18n.Message{ ID: "CantFindHunk", - Other: `Could not find hunk`, + Other: `Nie można znaleźć kawałka`, }, ) } diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index 4ec374616..a08a2edcc 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -36,25 +36,24 @@ type Updaterer interface { Update() } -var ( - projectUrl = "https://github.com/jesseduffield/lazygit" +const ( + PROJECT_URL = "https://github.com/jesseduffield/lazygit" ) // NewUpdater creates a new updater func NewUpdater(log *logrus.Entry, config config.AppConfigurer, osCommand *commands.OSCommand, tr *i18n.Localizer) (*Updater, error) { contextLogger := log.WithField("context", "updates") - updater := &Updater{ + return &Updater{ Log: contextLogger, Config: config, OSCommand: osCommand, Tr: tr, - } - return updater, nil + }, nil } func (u *Updater) getLatestVersionNumber() (string, error) { - req, err := http.NewRequest("GET", projectUrl+"/releases/latest", nil) + req, err := http.NewRequest("GET", PROJECT_URL+"/releases/latest", nil) if err != nil { return "", err } @@ -65,17 +64,16 @@ func (u *Updater) getLatestVersionNumber() (string, error) { return "", err } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { + + dec := json.NewDecoder(resp.Body) + data := struct { + TagName string `json:"tag_name"` + }{} + if err := dec.Decode(&data); err != nil { return "", err } - byt := []byte(body) - var dat map[string]interface{} - if err := json.Unmarshal(byt, &dat); err != nil { - return "", err - } - return dat["tag_name"].(string), nil + return data.TagName, nil } // RecordLastUpdateCheck records last time an update check was performed @@ -225,14 +223,14 @@ func (u *Updater) getBinaryUrl(newVersion string) (string, error) { } url := fmt.Sprintf( "%s/releases/download/%s/lazygit_%s_%s_%s.%s", - projectUrl, + PROJECT_URL, newVersion, newVersion[1:], u.mappedOs(runtime.GOOS), u.mappedArch(runtime.GOARCH), extension, ) - u.Log.Info("url for latest release is " + url) + u.Log.Info("Url for latest release is " + url) return url, nil } @@ -251,7 +249,7 @@ func (u *Updater) update(newVersion string) error { if err != nil { return err } - u.Log.Info("updating with url " + rawUrl) + u.Log.Info("Updating with url " + rawUrl) return u.downloadAndInstall(rawUrl) } @@ -267,7 +265,7 @@ func (u *Updater) downloadAndInstall(rawUrl string) error { return err } defer os.RemoveAll(tempDir) - u.Log.Info("temp directory is " + tempDir) + u.Log.Info("Temp directory is " + tempDir) // Get it! if err := g.Get(tempDir, url); err != nil { @@ -279,14 +277,14 @@ func (u *Updater) downloadAndInstall(rawUrl string) error { if err != nil { return err } - u.Log.Info("binary path is " + binaryPath) + u.Log.Info("Binary path is " + binaryPath) binaryName := filepath.Base(binaryPath) - u.Log.Info("binary name is " + binaryName) + u.Log.Info("Binary name is " + binaryName) // Verify the main file exists tempPath := filepath.Join(tempDir, binaryName) - u.Log.Info("temp path to binary is " + tempPath) + u.Log.Info("Temp path to binary is " + tempPath) if _, err := os.Stat(tempPath); err != nil { return err } @@ -296,7 +294,7 @@ func (u *Updater) downloadAndInstall(rawUrl string) error { if err != nil { return err } - u.Log.Info("update complete!") + u.Log.Info("Update complete!") return nil } diff --git a/scripts/bump_modules.sh b/scripts/bump_modules.sh new file mode 100755 index 000000000..ffcb0834f --- /dev/null +++ b/scripts/bump_modules.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +GO111MODULE=on +mv go.mod /tmp/ +go mod init \ No newline at end of file diff --git a/scripts/generate_cheatsheet.go b/scripts/generate_cheatsheet.go new file mode 100644 index 000000000..f427c29fa --- /dev/null +++ b/scripts/generate_cheatsheet.go @@ -0,0 +1,63 @@ +// This "script" generates a file called Keybindings_{{.LANG}}.md +// in current working directory. +// +// The content of this generated file is a keybindings cheatsheet. +// +// To generate cheatsheet in english run: +// LANG=en go run scripts/generate_cheatsheet.go + +package main + +import ( + "fmt" + "github.com/jesseduffield/lazygit/pkg/app" + "github.com/jesseduffield/lazygit/pkg/config" + "log" + "os" + "strings" +) + +func writeString(file *os.File, str string) { + _, err := file.WriteString(str) + if err != nil { + log.Fatal(err) + } +} + +func getTitle(mApp *app.App, viewName string) string { + viewTitle := strings.Title(viewName) + "Title" + translatedTitle := mApp.Tr.SLocalize(viewTitle) + formattedTitle := fmt.Sprintf("\n## %s\n\n", translatedTitle) + return formattedTitle +} + +func main() { + mConfig, _ := config.NewAppConfig("", "", "", "", "", new(bool)) + mApp, _ := app.Setup(mConfig) + lang := mApp.Tr.GetLanguage() + file, _ := os.Create("Keybindings_" + lang + ".md") + current := "" + + writeString(file, fmt.Sprintf("# Lazygit %s\n", mApp.Tr.SLocalize("menu"))) + writeString(file, getTitle(mApp, "global")) + + writeString(file, "
\n")
+
+	for _, binding := range mApp.Gui.GetKeybindings() {
+		if binding.Description == "" {
+			continue
+		}
+
+		if binding.ViewName != current {
+			current = binding.ViewName
+			writeString(file, "
\n") + writeString(file, getTitle(mApp, current)) + writeString(file, "
\n")
+		}
+
+		info := fmt.Sprintf("  %s: %s\n", binding.GetKey(), binding.Description)
+		writeString(file, info)
+	}
+
+	writeString(file, "
\n") +} diff --git a/test/hooks/pre-push b/test/hooks/pre-push new file mode 100644 index 000000000..b7cb2e87b --- /dev/null +++ b/test/hooks/pre-push @@ -0,0 +1,25 @@ +#!/bin/bash + +# test pre-push hook for testing the lazygit credentials view +# +# to enable, use: +# chmod +x .git/hooks/pre-push +# +# this will hang if you're using git from the command line, so only enable this +# when you are testing the credentials view in lazygit + +exec < /dev/tty + +echo -n "Username for 'github': " +read username + +echo -n "Password for 'github': " +read password + +if [ "$username" = "username" -a "$password" = "password" ]; then + echo "success" + exit 0 +fi + +>&2 echo "incorrect username/password" +exit 1 diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index 939d1bdfa..73df1dbcf 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -148,7 +148,6 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error { if x < 0 || x >= maxX || y < 0 || y >= maxY { return errors.New("invalid point") } - var ( ry, rcy int err error @@ -270,12 +269,19 @@ func (v *View) parseInput(ch rune) []cell { if isEscape { return nil } - c := cell{ - fgColor: v.ei.curFgColor, - bgColor: v.ei.curBgColor, - chr: ch, + repeatCount := 1 + if ch == '\t' { + ch = ' ' + repeatCount = 4 + } + for i := 0; i < repeatCount; i++ { + c := cell{ + fgColor: v.ei.curFgColor, + bgColor: v.ei.curBgColor, + chr: ch, + } + cells = append(cells, c) } - cells = append(cells, c) } return cells @@ -533,7 +539,7 @@ func lineWrap(line []cell, columns int) [][]cell { n += rw if n > columns { n = rw - lines = append(lines, line[offset:i-1]) + lines = append(lines, line[offset:i]) offset = i } } diff --git a/vendor/github.com/jesseduffield/pty/License b/vendor/github.com/jesseduffield/pty/License new file mode 100644 index 000000000..6b7558b6b --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/License @@ -0,0 +1,23 @@ +Copyright (c) 2011 Keith Rarick + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall +be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/jesseduffield/pty/doc.go b/vendor/github.com/jesseduffield/pty/doc.go new file mode 100644 index 000000000..190cfbea9 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/doc.go @@ -0,0 +1,16 @@ +// Package pty provides functions for working with Unix terminals. +package pty + +import ( + "errors" + "os" +) + +// ErrUnsupported is returned if a function is not +// available on the current platform. +var ErrUnsupported = errors.New("unsupported") + +// Opens a pty and its corresponding tty. +func Open() (pty, tty *os.File, err error) { + return open() +} diff --git a/vendor/github.com/jesseduffield/pty/ioctl.go b/vendor/github.com/jesseduffield/pty/ioctl.go new file mode 100644 index 000000000..c57c19e7e --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ioctl.go @@ -0,0 +1,13 @@ +// +build !windows + +package pty + +import "syscall" + +func ioctl(fd, cmd, ptr uintptr) error { + _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr) + if e != 0 { + return e + } + return nil +} diff --git a/vendor/github.com/jesseduffield/pty/ioctl_bsd.go b/vendor/github.com/jesseduffield/pty/ioctl_bsd.go new file mode 100644 index 000000000..73b12c53c --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ioctl_bsd.go @@ -0,0 +1,39 @@ +// +build darwin dragonfly freebsd netbsd openbsd + +package pty + +// from +const ( + _IOC_VOID uintptr = 0x20000000 + _IOC_OUT uintptr = 0x40000000 + _IOC_IN uintptr = 0x80000000 + _IOC_IN_OUT uintptr = _IOC_OUT | _IOC_IN + _IOC_DIRMASK = _IOC_VOID | _IOC_OUT | _IOC_IN + + _IOC_PARAM_SHIFT = 13 + _IOC_PARAM_MASK = (1 << _IOC_PARAM_SHIFT) - 1 +) + +func _IOC_PARM_LEN(ioctl uintptr) uintptr { + return (ioctl >> 16) & _IOC_PARAM_MASK +} + +func _IOC(inout uintptr, group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return inout | (param_len&_IOC_PARAM_MASK)<<16 | uintptr(group)<<8 | ioctl_num +} + +func _IO(group byte, ioctl_num uintptr) uintptr { + return _IOC(_IOC_VOID, group, ioctl_num, 0) +} + +func _IOR(group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return _IOC(_IOC_OUT, group, ioctl_num, param_len) +} + +func _IOW(group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return _IOC(_IOC_IN, group, ioctl_num, param_len) +} + +func _IOWR(group byte, ioctl_num uintptr, param_len uintptr) uintptr { + return _IOC(_IOC_IN_OUT, group, ioctl_num, param_len) +} diff --git a/vendor/github.com/jesseduffield/pty/pty_darwin.go b/vendor/github.com/jesseduffield/pty/pty_darwin.go new file mode 100644 index 000000000..6344b6b0e --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/pty_darwin.go @@ -0,0 +1,65 @@ +package pty + +import ( + "errors" + "os" + "syscall" + "unsafe" +) + +func open() (pty, tty *os.File, err error) { + pFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC, 0) + if err != nil { + return nil, nil, err + } + p := os.NewFile(uintptr(pFD), "/dev/ptmx") + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + if err := grantpt(p); err != nil { + return nil, nil, err + } + + if err := unlockpt(p); err != nil { + return nil, nil, err + } + + t, err := os.OpenFile(sname, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func ptsname(f *os.File) (string, error) { + n := make([]byte, _IOC_PARM_LEN(syscall.TIOCPTYGNAME)) + + err := ioctl(f.Fd(), syscall.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n[0]))) + if err != nil { + return "", err + } + + for i, c := range n { + if c == 0 { + return string(n[:i]), nil + } + } + return "", errors.New("TIOCPTYGNAME string not NUL-terminated") +} + +func grantpt(f *os.File) error { + return ioctl(f.Fd(), syscall.TIOCPTYGRANT, 0) +} + +func unlockpt(f *os.File) error { + return ioctl(f.Fd(), syscall.TIOCPTYUNLK, 0) +} diff --git a/vendor/github.com/jesseduffield/pty/pty_dragonfly.go b/vendor/github.com/jesseduffield/pty/pty_dragonfly.go new file mode 100644 index 000000000..b7d1f20f2 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/pty_dragonfly.go @@ -0,0 +1,80 @@ +package pty + +import ( + "errors" + "os" + "strings" + "syscall" + "unsafe" +) + +// same code as pty_darwin.go +func open() (pty, tty *os.File, err error) { + p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + if err := grantpt(p); err != nil { + return nil, nil, err + } + + if err := unlockpt(p); err != nil { + return nil, nil, err + } + + t, err := os.OpenFile(sname, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func grantpt(f *os.File) error { + _, err := isptmaster(f.Fd()) + return err +} + +func unlockpt(f *os.File) error { + _, err := isptmaster(f.Fd()) + return err +} + +func isptmaster(fd uintptr) (bool, error) { + err := ioctl(fd, syscall.TIOCISPTMASTER, 0) + return err == nil, err +} + +var ( + emptyFiodgnameArg fiodgnameArg + ioctl_FIODNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg)) +) + +func ptsname(f *os.File) (string, error) { + name := make([]byte, _C_SPECNAMELEN) + fa := fiodgnameArg{Name: (*byte)(unsafe.Pointer(&name[0])), Len: _C_SPECNAMELEN, Pad_cgo_0: [4]byte{0, 0, 0, 0}} + + err := ioctl(f.Fd(), ioctl_FIODNAME, uintptr(unsafe.Pointer(&fa))) + if err != nil { + return "", err + } + + for i, c := range name { + if c == 0 { + s := "/dev/" + string(name[:i]) + return strings.Replace(s, "ptm", "pts", -1), nil + } + } + return "", errors.New("TIOCPTYGNAME string not NUL-terminated") +} diff --git a/vendor/github.com/jesseduffield/pty/pty_freebsd.go b/vendor/github.com/jesseduffield/pty/pty_freebsd.go new file mode 100644 index 000000000..63b6d9133 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/pty_freebsd.go @@ -0,0 +1,78 @@ +package pty + +import ( + "errors" + "os" + "syscall" + "unsafe" +) + +func posixOpenpt(oflag int) (fd int, err error) { + r0, _, e1 := syscall.Syscall(syscall.SYS_POSIX_OPENPT, uintptr(oflag), 0, 0) + fd = int(r0) + if e1 != 0 { + err = e1 + } + return fd, err +} + +func open() (pty, tty *os.File, err error) { + fd, err := posixOpenpt(syscall.O_RDWR | syscall.O_CLOEXEC) + if err != nil { + return nil, nil, err + } + p := os.NewFile(uintptr(fd), "/dev/pts") + // In case of error after this point, make sure we close the pts fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + t, err := os.OpenFile("/dev/"+sname, os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func isptmaster(fd uintptr) (bool, error) { + err := ioctl(fd, syscall.TIOCPTMASTER, 0) + return err == nil, err +} + +var ( + emptyFiodgnameArg fiodgnameArg + ioctlFIODGNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg)) +) + +func ptsname(f *os.File) (string, error) { + master, err := isptmaster(f.Fd()) + if err != nil { + return "", err + } + if !master { + return "", syscall.EINVAL + } + + const n = _C_SPECNAMELEN + 1 + var ( + buf = make([]byte, n) + arg = fiodgnameArg{Len: n, Buf: (*byte)(unsafe.Pointer(&buf[0]))} + ) + if err := ioctl(f.Fd(), ioctlFIODGNAME, uintptr(unsafe.Pointer(&arg))); err != nil { + return "", err + } + + for i, c := range buf { + if c == 0 { + return string(buf[:i]), nil + } + } + return "", errors.New("FIODGNAME string not NUL-terminated") +} diff --git a/vendor/github.com/jesseduffield/pty/pty_linux.go b/vendor/github.com/jesseduffield/pty/pty_linux.go new file mode 100644 index 000000000..296dd2129 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/pty_linux.go @@ -0,0 +1,51 @@ +package pty + +import ( + "os" + "strconv" + "syscall" + "unsafe" +) + +func open() (pty, tty *os.File, err error) { + p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0) + if err != nil { + return nil, nil, err + } + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(p) + if err != nil { + return nil, nil, err + } + + if err := unlockpt(p); err != nil { + return nil, nil, err + } + + t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0) + if err != nil { + return nil, nil, err + } + return p, t, nil +} + +func ptsname(f *os.File) (string, error) { + var n _C_uint + err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))) + if err != nil { + return "", err + } + return "/dev/pts/" + strconv.Itoa(int(n)), nil +} + +func unlockpt(f *os.File) error { + var u _C_int + // use TIOCSPTLCK with a zero valued arg to clear the slave pty lock + return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) +} diff --git a/vendor/github.com/jesseduffield/pty/pty_openbsd.go b/vendor/github.com/jesseduffield/pty/pty_openbsd.go new file mode 100644 index 000000000..6e7aeae7c --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/pty_openbsd.go @@ -0,0 +1,33 @@ +package pty + +import ( + "os" + "syscall" + "unsafe" +) + +func open() (pty, tty *os.File, err error) { + /* + * from ptm(4): + * The PTMGET command allocates a free pseudo terminal, changes its + * ownership to the caller, revokes the access privileges for all previous + * users, opens the file descriptors for the master and slave devices and + * returns them to the caller in struct ptmget. + */ + + p, err := os.OpenFile("/dev/ptm", os.O_RDWR|syscall.O_CLOEXEC, 0) + if err != nil { + return nil, nil, err + } + defer p.Close() + + var ptm ptmget + if err := ioctl(p.Fd(), uintptr(ioctl_PTMGET), uintptr(unsafe.Pointer(&ptm))); err != nil { + return nil, nil, err + } + + pty = os.NewFile(uintptr(ptm.Cfd), "/dev/ptm") + tty = os.NewFile(uintptr(ptm.Sfd), "/dev/ptm") + + return pty, tty, nil +} diff --git a/vendor/github.com/jesseduffield/pty/pty_unsupported.go b/vendor/github.com/jesseduffield/pty/pty_unsupported.go new file mode 100644 index 000000000..9a3e721bc --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/pty_unsupported.go @@ -0,0 +1,11 @@ +// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd + +package pty + +import ( + "os" +) + +func open() (pty, tty *os.File, err error) { + return nil, nil, ErrUnsupported +} diff --git a/vendor/github.com/jesseduffield/pty/run.go b/vendor/github.com/jesseduffield/pty/run.go new file mode 100644 index 000000000..dda19b760 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/run.go @@ -0,0 +1,54 @@ +// +build !windows + +package pty + +import ( + "os" + "os/exec" + "syscall" +) + +// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout, +// and c.Stderr, calls c.Start, and returns the File of the tty's +// corresponding pty. +func Start(c *exec.Cmd) (pty *os.File, err error) { + return StartWithSize(c, nil) +} + +// StartWithSize assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout, +// and c.Stderr, calls c.Start, and returns the File of the tty's +// corresponding pty. +// +// This will resize the pty to the specified size before starting the command +func StartWithSize(c *exec.Cmd, sz *Winsize) (pty *os.File, err error) { + pty, tty, err := Open() + if err != nil { + return nil, err + } + defer tty.Close() + if sz != nil { + err = Setsize(pty, sz) + if err != nil { + pty.Close() + return nil, err + } + } + if c.Stdout == nil { + c.Stdout = tty + } + if c.Stderr == nil { + c.Stderr = tty + } + c.Stdin = tty + if c.SysProcAttr == nil { + c.SysProcAttr = &syscall.SysProcAttr{} + } + c.SysProcAttr.Setctty = true + c.SysProcAttr.Setsid = true + err = c.Start() + if err != nil { + pty.Close() + return nil, err + } + return pty, err +} diff --git a/vendor/github.com/jesseduffield/pty/types.go b/vendor/github.com/jesseduffield/pty/types.go new file mode 100644 index 000000000..5aecb6bcd --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/types.go @@ -0,0 +1,10 @@ +// +build ignore + +package pty + +import "C" + +type ( + _C_int C.int + _C_uint C.uint +) diff --git a/vendor/github.com/jesseduffield/pty/types_dragonfly.go b/vendor/github.com/jesseduffield/pty/types_dragonfly.go new file mode 100644 index 000000000..5c0493b85 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/types_dragonfly.go @@ -0,0 +1,17 @@ +// +build ignore + +package pty + +/* +#define _KERNEL +#include +#include +#include +*/ +import "C" + +const ( + _C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */ +) + +type fiodgnameArg C.struct_fiodname_args diff --git a/vendor/github.com/jesseduffield/pty/types_freebsd.go b/vendor/github.com/jesseduffield/pty/types_freebsd.go new file mode 100644 index 000000000..ce3eb9518 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/types_freebsd.go @@ -0,0 +1,15 @@ +// +build ignore + +package pty + +/* +#include +#include +*/ +import "C" + +const ( + _C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */ +) + +type fiodgnameArg C.struct_fiodgname_arg diff --git a/vendor/github.com/jesseduffield/pty/types_openbsd.go b/vendor/github.com/jesseduffield/pty/types_openbsd.go new file mode 100644 index 000000000..47701b5f9 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/types_openbsd.go @@ -0,0 +1,14 @@ +// +build ignore + +package pty + +/* +#include +#include +#include +*/ +import "C" + +type ptmget C.struct_ptmget + +var ioctl_PTMGET = C.PTMGET diff --git a/vendor/github.com/jesseduffield/pty/util.go b/vendor/github.com/jesseduffield/pty/util.go new file mode 100644 index 000000000..68a8584cf --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/util.go @@ -0,0 +1,64 @@ +// +build !windows + +package pty + +import ( + "os" + "syscall" + "unsafe" +) + +// InheritSize applies the terminal size of master to slave. This should be run +// in a signal handler for syscall.SIGWINCH to automatically resize the slave when +// the master receives a window size change notification. +func InheritSize(master, slave *os.File) error { + size, err := GetsizeFull(master) + if err != nil { + return err + } + err = Setsize(slave, size) + if err != nil { + return err + } + return nil +} + +// Setsize resizes t to s. +func Setsize(t *os.File, ws *Winsize) error { + return windowRectCall(ws, t.Fd(), syscall.TIOCSWINSZ) +} + +// GetsizeFull returns the full terminal size description. +func GetsizeFull(t *os.File) (size *Winsize, err error) { + var ws Winsize + err = windowRectCall(&ws, t.Fd(), syscall.TIOCGWINSZ) + return &ws, err +} + +// Getsize returns the number of rows (lines) and cols (positions +// in each line) in terminal t. +func Getsize(t *os.File) (rows, cols int, err error) { + ws, err := GetsizeFull(t) + return int(ws.Rows), int(ws.Cols), err +} + +// Winsize describes the terminal size. +type Winsize struct { + Rows uint16 // ws_row: Number of rows (in cells) + Cols uint16 // ws_col: Number of columns (in cells) + X uint16 // ws_xpixel: Width in pixels + Y uint16 // ws_ypixel: Height in pixels +} + +func windowRectCall(ws *Winsize, fd, a2 uintptr) error { + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + fd, + a2, + uintptr(unsafe.Pointer(ws)), + ) + if errno != 0 { + return syscall.Errno(errno) + } + return nil +} diff --git a/vendor/github.com/jesseduffield/pty/ztypes_386.go b/vendor/github.com/jesseduffield/pty/ztypes_386.go new file mode 100644 index 000000000..ff0b8fd83 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_386.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/jesseduffield/pty/ztypes_amd64.go b/vendor/github.com/jesseduffield/pty/ztypes_amd64.go new file mode 100644 index 000000000..ff0b8fd83 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_amd64.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/jesseduffield/pty/ztypes_arm.go b/vendor/github.com/jesseduffield/pty/ztypes_arm.go new file mode 100644 index 000000000..ff0b8fd83 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_arm.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/jesseduffield/pty/ztypes_arm64.go b/vendor/github.com/jesseduffield/pty/ztypes_arm64.go new file mode 100644 index 000000000..6c29a4b91 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_arm64.go @@ -0,0 +1,11 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +// +build arm64 + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/jesseduffield/pty/ztypes_dragonfly_amd64.go b/vendor/github.com/jesseduffield/pty/ztypes_dragonfly_amd64.go new file mode 100644 index 000000000..6b0ba037f --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_dragonfly_amd64.go @@ -0,0 +1,14 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_dragonfly.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Name *byte + Len uint32 + Pad_cgo_0 [4]byte +} diff --git a/vendor/github.com/jesseduffield/pty/ztypes_freebsd_386.go b/vendor/github.com/jesseduffield/pty/ztypes_freebsd_386.go new file mode 100644 index 000000000..d9975374e --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_freebsd_386.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Buf *byte +} diff --git a/vendor/github.com/jesseduffield/pty/ztypes_freebsd_amd64.go b/vendor/github.com/jesseduffield/pty/ztypes_freebsd_amd64.go new file mode 100644 index 000000000..5fa102fcd --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_freebsd_amd64.go @@ -0,0 +1,14 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Pad_cgo_0 [4]byte + Buf *byte +} diff --git a/vendor/github.com/jesseduffield/pty/ztypes_freebsd_arm.go b/vendor/github.com/jesseduffield/pty/ztypes_freebsd_arm.go new file mode 100644 index 000000000..d9975374e --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_freebsd_arm.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Buf *byte +} diff --git a/vendor/github.com/jesseduffield/pty/ztypes_mipsx.go b/vendor/github.com/jesseduffield/pty/ztypes_mipsx.go new file mode 100644 index 000000000..f0ce74086 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_mipsx.go @@ -0,0 +1,12 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +// +build linux +// +build mips mipsle mips64 mips64le + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/jesseduffield/pty/ztypes_openbsd_386.go b/vendor/github.com/jesseduffield/pty/ztypes_openbsd_386.go new file mode 100644 index 000000000..ccb3aab9a --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_openbsd_386.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_openbsd.go + +package pty + +type ptmget struct { + Cfd int32 + Sfd int32 + Cn [16]int8 + Sn [16]int8 +} + +var ioctl_PTMGET = 0x40287401 diff --git a/vendor/github.com/jesseduffield/pty/ztypes_openbsd_amd64.go b/vendor/github.com/jesseduffield/pty/ztypes_openbsd_amd64.go new file mode 100644 index 000000000..e67051688 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_openbsd_amd64.go @@ -0,0 +1,13 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_openbsd.go + +package pty + +type ptmget struct { + Cfd int32 + Sfd int32 + Cn [16]int8 + Sn [16]int8 +} + +var ioctl_PTMGET = 0x40287401 diff --git a/vendor/github.com/jesseduffield/pty/ztypes_ppc64.go b/vendor/github.com/jesseduffield/pty/ztypes_ppc64.go new file mode 100644 index 000000000..4e1af8431 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_ppc64.go @@ -0,0 +1,11 @@ +// +build ppc64 + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/jesseduffield/pty/ztypes_ppc64le.go b/vendor/github.com/jesseduffield/pty/ztypes_ppc64le.go new file mode 100644 index 000000000..e6780f4e2 --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_ppc64le.go @@ -0,0 +1,11 @@ +// +build ppc64le + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/jesseduffield/pty/ztypes_s390x.go b/vendor/github.com/jesseduffield/pty/ztypes_s390x.go new file mode 100644 index 000000000..a7452b61c --- /dev/null +++ b/vendor/github.com/jesseduffield/pty/ztypes_s390x.go @@ -0,0 +1,11 @@ +// +build s390x + +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +)