diff options
59 files changed, 3593 insertions, 48 deletions
diff --git a/pkg/commands/exec_live_default.go b/pkg/commands/exec_live_default.go new file mode 100644 index 000000000..ad1c568e5 --- /dev/null +++ b/pkg/commands/exec_live_default.go @@ -0,0 +1,121 @@ +// +build !windows + +package commands + +import ( + "bufio" + "os" + "os/exec" + "strings" + "sync" + "unicode/utf8" + + "github.com/kr/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 +// NOTE: You don't have to include a enter in the return data this function will do that for you +func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) (errorMessage string, codeError error) { + cmdOutput := []string{} + + splitCmd := str.ToArgv(command) + cmd := exec.Command(splitCmd[0], splitCmd[1:]...) + + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "LANG=en_US.utf8", "LC_ALL=en_US.UTF-8") + + tty, err := pty.Start(cmd) + + if err != nil { + return errorMessage, err + } + + stopAsking := make(chan struct{}) + + var waitForBufio sync.WaitGroup + waitForBufio.Add(1) + + defer func() { + _ = tty.Close() + }() + + go func() { + scanner := bufio.NewScanner(tty) + scanner.Split(scanWordsWithNewLines) + for scanner.Scan() { + select { + case <-stopAsking: + // just do nothing + default: + toOutput := strings.Trim(scanner.Text(), " ") + cmdOutput = append(cmdOutput, toOutput) + toWrite := output(toOutput) + if len(toWrite) > 0 { + _, _ = tty.Write([]byte(toWrite + "\n")) + } + } + } + waitForBufio.Done() + }() + + err = cmd.Wait() + go func() { + stopAsking <- struct{}{} + }() + if err != nil { + waitForBufio.Wait() + return strings.Join(cmdOutput, " "), err + } + + return errorMessage, 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..d97274279 --- /dev/null +++ b/pkg/commands/exec_live_win.go @@ -0,0 +1,10 @@ +// +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 propper way to run commands live on windows +func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) (errorMessage string, codeError error) { + cmdOputput := c.RunCommand(command) + return cmdOputput.Error(), cmdOputput +} diff --git a/pkg/commands/git.go b/pkg/commands/git.go index b78db1fa6..d9230c4eb 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -261,8 +261,13 @@ func (c *GitCommand) RenameCommit(name string) error { } // Fetch fetch git repo -func (c *GitCommand) Fetch() error { - return c.OSCommand.RunCommand("git fetch") +func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canSskForCredentials bool) error { + return c.OSCommand.DetectUnamePass("git fetch", func(question string) string { + if canSskForCredentials { + return unamePassQuestion(question) + } + return "-" + }) } // ResetToCommit reset to commit @@ -340,18 +345,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..7d0a503d7 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.Equal(t, "exit status 128", err.Error()) }, }, { @@ -1023,7 +1023,7 @@ func TestGitCommandPush(t *testing.T) { }, true, func(err error) { - assert.Nil(t, err) + assert.Equal(t, "exit status 128", err.Error()) }, }, { @@ -1036,7 +1036,7 @@ func TestGitCommandPush(t *testing.T) { }, false, func(err error) { - assert.Error(t, err) + assert.Equal(t, "exit status 128", err.Error()) }, }, } @@ -1045,7 +1045,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 "-" + }) + s.test(err) }) } } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 6b28a69bb..faf6c5aec 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,53 @@ func (c *OSCommand) RunCommandWithOutput(command string) (string, error) { ) } +// 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 + + type Prompt struct { + pattern string + canAskFor bool + } + prompts := map[string]Prompt{ + "password": { + pattern: `Password\s*for\s*'.+':`, + canAskFor: true, + }, + "username": { + pattern: `Username\s*for\s*'.+':`, + canAskFor: true, + }, + } + + for askFor, prompt := range prompts { + if match, _ := regexp.MatchString(prompt.pattern, ttyText); match && prompt.canAskFor { + prompt.canAskFor = false + 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) diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index f789349b4..f0d9e7015 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -236,15 +236,17 @@ confirmOnQuit: false // AppState stores data between runs of the app like when the last update check // was performed and which other repos have been checked out type AppState struct { - LastUpdateCheck int64 - RecentRepos []string + LastUpdateCheck int64 + RecentRepos []string + RecentPrivateRepos []string } func getDefaultAppState() []byte { - return []byte(` - lastUpdateCheck: 0 - recentRepos: [] -`) + return []byte(` + lastUpdateCheck: 0 + recentRepos: [] + RecentPrivateRepos: [] + `) } // // commenting this out until we use it again diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index a0e3448f6..e54d6e8c1 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -124,6 +124,32 @@ 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) + if err != nil { + errMessage := err.Error() + if errMessage == "exit status 128" { + errMessage = gui.Tr.SLocalize("PassUnameWrong") + } + _ = gui.createErrorPanel(g, errMessage) + } + if unamePassOpend { + _, _ = g.SetViewOnBottom("pushPassUname") + _ = g.DeleteView("pushPassUname") + } + if err == nil { + _ = gui.closeConfirmationPrompt(g) + _ = gui.refreshCommits(g) + _ = gui.refreshStatus(g) + } + }() + return nil +} + func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error { branch := gui.getSelectedBranch() message := gui.Tr.SLocalize("SureForceCheckout") @@ -186,14 +212,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, }, @@ -203,9 +229,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/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 33eb5b218..85497ef87 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -51,6 +51,89 @@ func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error { return gui.renderString(g, "options", message) } +type credentials chan string + +// waitForPassUname wait for a username or password input from the pushPassUname popup +func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUname string) string { + gui.credentials = make(chan string) + pushPassUnameView, _ := g.View("pushPassUname") + if passOrUname == "username" { + pushPassUnameView.Title = gui.Tr.SLocalize("PushUsername") + pushPassUnameView.Mask = 0 + } else { + pushPassUnameView.Title = gui.Tr.SLocalize("PushPassword") + pushPassUnameView.Mask = '*' + } + g.Update(func(g *gocui.Gui) error { + _, err := g.SetViewOnTop("pushPassUname") + if err != nil { + return err + } + err = gui.switchFocus(g, currentView, pushPassUnameView) + if err != nil { + return err + } + gui.RenderCommitLength() + return nil + }) + + // wait for username/passwords input + userInput := <-gui.credentials + return userInput +} + +func (gui *Gui) handlePushConfirm(g *gocui.Gui, v *gocui.View) error { + message := gui.trimmedContent(v) + if message == "" { + // make sure to input something + // if not dune the push progress will run forever + message = "-" + } + 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("pushPassUname") + if err != nil { + return err + } + err = gui.switchFocus(g, v, gui.getFilesView(g)) + if err != nil { + return err + } + return gui.refreshCommits(g) +} + +func (gui *Gui) handlePushClose(g *gocui.Gui, v *gocui.View) error { + _, err := g.SetViewOnBottom("pushPassUname") + if err != nil { + return err + } + gui.credentials <- "-" + return gui.switchFocus(g, v, gui.getFilesView(g)) +} + +func (gui *Gui) handlePushFocused(g *gocui.Gui, v *gocui.View) error { + if _, err := g.SetViewOnTop("pushPassUname"); err != nil { + return err + } + + message := gui.Tr.TemplateLocalize( + "CloseConfirm", + Teml{ + "keyBindClose": "esc", + "keyBindConfirm": "enter", + }, + ) + return gui.renderString(g, "options", message) +} + |