1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-02-21 19:48:58 +02:00

Merge pull request #12 from jesseduffield/master

Update to latest master
This commit is contained in:
Mark Kopenga 2018-09-14 11:50:49 +02:00 committed by GitHub
commit c64fb87b2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 477 additions and 87 deletions

View File

@ -3,6 +3,7 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"log"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -40,13 +41,13 @@ func main() {
} }
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag) appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
if err != nil { if err != nil {
panic(err) log.Fatal(err.Error())
} }
app, err := app.Setup(appConfig) app, err := app.Setup(appConfig)
if err != nil { if err != nil {
app.Log.Error(err.Error()) app.Log.Error(err.Error())
panic(err) log.Fatal(err.Error())
} }
app.Gui.RunWithSubprocesses() app.Gui.RunWithSubprocesses()

View File

@ -7,7 +7,6 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -59,11 +58,13 @@ func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repositor
// GitCommand is our main git interface // GitCommand is our main git interface
type GitCommand struct { type GitCommand struct {
Log *logrus.Entry Log *logrus.Entry
OSCommand *OSCommand OSCommand *OSCommand
Worktree *gogit.Worktree Worktree *gogit.Worktree
Repo *gogit.Repository Repo *gogit.Repository
Tr *i18n.Localizer Tr *i18n.Localizer
getGlobalGitConfig func(string) (string, error)
getLocalGitConfig func(string) (string, error)
} }
// NewGitCommand it runs git commands // NewGitCommand it runs git commands
@ -92,11 +93,13 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer)
} }
return &GitCommand{ return &GitCommand{
Log: log, Log: log,
OSCommand: osCommand, OSCommand: osCommand,
Tr: tr, Tr: tr,
Worktree: worktree, Worktree: worktree,
Repo: repo, Repo: repo,
getGlobalGitConfig: gitconfig.Global,
getLocalGitConfig: gitconfig.Local,
}, nil }, nil
} }
@ -170,28 +173,37 @@ func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
return newFiles return newFiles
} }
headResults := []File{} appendedIndexes := []int{}
tailResults := []File{}
for _, newFile := range newFiles { // retain position of files we already could see
var isHeadResult bool result := []File{}
for _, oldFile := range oldFiles {
for _, oldFile := range oldFiles { for newIndex, newFile := range newFiles {
if oldFile.Name == newFile.Name { if oldFile.Name == newFile.Name {
isHeadResult = true result = append(result, newFile)
appendedIndexes = append(appendedIndexes, newIndex)
break break
} }
} }
if isHeadResult {
headResults = append(headResults, newFile)
continue
}
tailResults = append(tailResults, newFile)
} }
return append(headResults, tailResults...) // append any new files to the end
for index, newFile := range newFiles {
if !includesInt(appendedIndexes, index) {
result = append(result, newFile)
}
}
return result
}
func includesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
} }
// GetBranchName branch name // GetBranchName branch name
@ -274,89 +286,84 @@ func (c *GitCommand) AbortMerge() error {
return c.OSCommand.RunCommand("git merge --abort") return c.OSCommand.RunCommand("git merge --abort")
} }
// UsingGpg tells us whether the user has gpg enabled so that we can know // usingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password // whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) UsingGpg() bool { func (c *GitCommand) usingGpg() bool {
gpgsign, _ := gitconfig.Global("commit.gpgsign") gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
if gpgsign == "" { if gpgsign == "" {
gpgsign, _ = gitconfig.Local("commit.gpgsign") gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
} }
if gpgsign == "" { value := strings.ToLower(gpgsign)
return false
} return value == "true" || value == "1" || value == "yes" || value == "on"
return true
} }
// Commit commit to git // Commit commits to git
func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) { func (c *GitCommand) Commit(message string) (*exec.Cmd, error) {
command := "git commit -m " + c.OSCommand.Quote(message) command := fmt.Sprintf("git commit -m %s", c.OSCommand.Quote(message))
if c.UsingGpg() { if c.usingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
} }
return nil, c.OSCommand.RunCommand(command) return nil, c.OSCommand.RunCommand(command)
} }
// Pull pull from repo // Pull pulls from repo
func (c *GitCommand) Pull() error { func (c *GitCommand) Pull() error {
return c.OSCommand.RunCommand("git pull --no-edit") return c.OSCommand.RunCommand("git pull --no-edit")
} }
// Push push to a branch // Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool) error { func (c *GitCommand) Push(branchName string, force bool) error {
forceFlag := "" forceFlag := ""
if force { if force {
forceFlag = "--force-with-lease " forceFlag = "--force-with-lease "
} }
return c.OSCommand.RunCommand("git push " + forceFlag + "-u origin " + branchName)
return c.OSCommand.RunCommand(fmt.Sprintf("git push %s -u origin %s", forceFlag, branchName))
} }
// SquashPreviousTwoCommits squashes a commit down to the one below it // SquashPreviousTwoCommits squashes a commit down to the one below it
// retaining the message of the higher commit // retaining the message of the higher commit
func (c *GitCommand) SquashPreviousTwoCommits(message string) error { func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
// TODO: test this // TODO: test this
err := c.OSCommand.RunCommand("git reset --soft HEAD^") if err := c.OSCommand.RunCommand("git reset --soft HEAD^"); err != nil {
if err != nil {
return err return err
} }
// TODO: if password is required, we need to return a subprocess // TODO: if password is required, we need to return a subprocess
return c.OSCommand.RunCommand("git commit --amend -m " + c.OSCommand.Quote(message)) return c.OSCommand.RunCommand(fmt.Sprintf("git commit --amend -m %s", c.OSCommand.Quote(message)))
} }
// SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it, // SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it,
// retaining the commit message of the lower commit // retaining the commit message of the lower commit
func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error { func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
var err error
commands := []string{ commands := []string{
"git checkout -q " + shaValue, fmt.Sprintf("git checkout -q %s", shaValue),
"git reset --soft " + shaValue + "^", fmt.Sprintf("git reset --soft %s^", shaValue),
"git commit --amend -C " + shaValue + "^", fmt.Sprintf("git commit --amend -C %s^", shaValue),
"git rebase --onto HEAD " + shaValue + " " + branchName, fmt.Sprintf("git rebase --onto HEAD %s %s", shaValue, branchName),
} }
ret := ""
for _, command := range commands { for _, command := range commands {
c.Log.Info(command) c.Log.Info(command)
output, err := c.OSCommand.RunCommandWithOutput(command)
ret += output if output, err := c.OSCommand.RunCommandWithOutput(command); err != nil {
if err != nil { ret := output
// We are already in an error state here so we're just going to append
// the output of these commands
output, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git branch -d %s", shaValue))
ret += output
output, _ = c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git checkout %s", branchName))
ret += output
c.Log.Info(ret) c.Log.Info(ret)
break return errors.New(ret)
} }
} }
if err != nil {
// We are already in an error state here so we're just going to append
// the output of these commands
output, _ := c.OSCommand.RunCommandWithOutput("git branch -d " + shaValue)
ret += output
output, _ = c.OSCommand.RunCommandWithOutput("git checkout " + branchName)
ret += output
}
if err != nil {
return errors.New(ret)
}
return nil return nil
} }
// CatFile obtain the contents of a file // CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) { func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("cat " + c.OSCommand.Quote(fileName)) return c.OSCommand.RunCommandWithOutput("cat " + c.OSCommand.Quote(fileName))
} }

View File

@ -56,9 +56,11 @@ func newDummyLog() *logrus.Entry {
func newDummyGitCommand() *GitCommand { func newDummyGitCommand() *GitCommand {
return &GitCommand{ return &GitCommand{
Log: newDummyLog(), Log: newDummyLog(),
OSCommand: newDummyOSCommand(), OSCommand: newDummyOSCommand(),
Tr: i18n.NewLocalizer(newDummyLog()), Tr: i18n.NewLocalizer(newDummyLog()),
getGlobalGitConfig: func(string) (string, error) { return "", nil },
getLocalGitConfig: func(string) (string, error) { return "", nil },
} }
} }
@ -730,6 +732,353 @@ func TestGitCommandMerge(t *testing.T) {
assert.NoError(t, gitCmd.Merge("test")) assert.NoError(t, gitCmd.Merge("test"))
} }
func TestGitCommandUsingGpg(t *testing.T) {
type scenario struct {
testName string
getLocalGitConfig func(string) (string, error)
getGlobalGitConfig func(string) (string, error)
test func(bool)
}
scenarios := []scenario{
{
"Option global and local config commit.gpgsign is not set",
func(string) (string, error) {
return "", nil
},
func(string) (string, error) {
return "", nil
},
func(gpgEnabled bool) {
assert.False(t, gpgEnabled)
},
},
{
"Option global config commit.gpgsign is not set, fallback on local config",
func(string) (string, error) {
return "", nil
},
func(string) (string, error) {
return "true", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
{
"Option commit.gpgsign is true",
func(string) (string, error) {
return "True", nil
},
func(string) (string, error) {
return "", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
{
"Option commit.gpgsign is on",
func(string) (string, error) {
return "ON", nil
},
func(string) (string, error) {
return "", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
{
"Option commit.gpgsign is yes",
func(string) (string, error) {
return "YeS", nil
},
func(string) (string, error) {
return "", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
{
"Option commit.gpgsign is 1",
func(string) (string, error) {
return "1", nil
},
func(string) (string, error) {
return "", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.getLocalGitConfig = s.getLocalGitConfig
s.test(gitCmd.usingGpg())
})
}
}
func TestGitCommandCommit(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
getGlobalGitConfig func(string) (string, error)
test func(*exec.Cmd, error)
}
scenarios := []scenario{
{
"Commit using gpg",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "bash", cmd)
assert.EqualValues(t, []string{"-c", `git commit -m 'test'`}, args)
return exec.Command("echo")
},
func(string) (string, error) {
return "true", nil
},
func(cmd *exec.Cmd, err error) {
assert.NotNil(t, cmd)
assert.Nil(t, err)
},
},
{
"Commit without using gpg",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "-m", "test"}, args)
return exec.Command("echo")
},
func(string) (string, error) {
return "false", nil
},
func(cmd *exec.Cmd, err error) {
assert.Nil(t, cmd)
assert.Nil(t, err)
},
},
{
"Commit without using gpg with an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "-m", "test"}, args)
return exec.Command("exit", "1")
},
func(string) (string, error) {
return "false", nil
},
func(cmd *exec.Cmd, err error) {
assert.Nil(t, cmd)
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Commit("test"))
})
}
}
func TestGitCommandPush(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
forcePush bool
test func(error)
}
scenarios := []scenario{
{
"Push with force disabled",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "-u", "origin", "test"}, args)
return exec.Command("echo")
},
false,
func(err error) {
assert.Nil(t, err)
},
},
{
"Push with force enable",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--force-with-lease", "-u", "origin", "test"}, args)
return exec.Command("echo")
},
true,
func(err error) {
assert.Nil(t, err)
},
},
{
"Push with an error occurring",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "-u", "origin", "test"}, args)
return exec.Command("exit", "1")
},
false,
func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Push("test", s.forcePush))
})
}
}
func TestGitCommandSquashPreviousTwoCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"Git reset triggers an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "--soft", "HEAD^"}, args)
return exec.Command("exit", "1")
},
func(err error) {
assert.NotNil(t, err)
},
},
{
"Git commit triggers an error",
func(cmd string, args ...string) *exec.Cmd {
if len(args) > 0 && args[0] == "reset" {
return exec.Command("echo")
}
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "-m", "test"}, args)
return exec.Command("exit", "1")
},
func(err error) {
assert.NotNil(t, err)
},
},
{
"Stash succeeded",
func(cmd string, args ...string) *exec.Cmd {
if len(args) > 0 && args[0] == "reset" {
return exec.Command("echo")
}
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "-m", "test"}, args)
return exec.Command("echo")
},
func(err error) {
assert.Nil(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.SquashPreviousTwoCommits("test"))
})
}
}
func TestGitCommandSquashFixupCommit(t *testing.T) {
type scenario struct {
testName string
command func() (func(string, ...string) *exec.Cmd, *[][]string)
test func(*[][]string, error)
}
scenarios := []scenario{
{
"An error occurred with one of the sub git command",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
if len(args) > 0 && args[0] == "checkout" {
return exec.Command("exit", "1")
}
return exec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.NotNil(t, err)
assert.Len(t, *cmdsCalled, 3)
assert.EqualValues(t, *cmdsCalled, [][]string{
{"checkout", "-q", "6789abcd"},
{"branch", "-d", "6789abcd"},
{"checkout", "test"},
})
},
},
{
"Squash fixup succeeded",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return exec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.Nil(t, err)
assert.Len(t, *cmdsCalled, 4)
assert.EqualValues(t, *cmdsCalled, [][]string{
{"checkout", "-q", "6789abcd"},
{"reset", "--soft", "6789abcd^"},
{"commit", "--amend", "-C", "6789abcd^"},
{"rebase", "--onto", "HEAD", "6789abcd", "test"},
})
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
var cmdsCalled *[][]string
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command, cmdsCalled = s.command()
s.test(cmdsCalled, gitCmd.SquashFixupCommit("test", "6789abcd"))
})
}
}
func TestGitCommandDiff(t *testing.T) { func TestGitCommandDiff(t *testing.T) {
gitCommand := newDummyGitCommand() gitCommand := newDummyGitCommand()
assert.NoError(t, test.GenerateRepo("lots_of_diffs.sh")) assert.NoError(t, test.GenerateRepo("lots_of_diffs.sh"))

View File

@ -17,11 +17,12 @@ import (
// Platform stores the os state // Platform stores the os state
type Platform struct { type Platform struct {
os string os string
shell string shell string
shellArg string shellArg string
escapedQuote string escapedQuote string
openCommand string openCommand string
fallbackEscapedQuote string
} }
// OSCommand holds all the os commands // OSCommand holds all the os commands
@ -140,7 +141,11 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
// Quote wraps a message in platform-specific quotation marks // Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string { func (c *OSCommand) Quote(message string) string {
message = strings.Replace(message, "`", "\\`", -1) message = strings.Replace(message, "`", "\\`", -1)
return c.Platform.escapedQuote + message + c.Platform.escapedQuote 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 // Unquote removes wrapping quotations marks if they are present

View File

@ -8,10 +8,11 @@ import (
func getPlatform() *Platform { func getPlatform() *Platform {
return &Platform{ return &Platform{
os: runtime.GOOS, os: runtime.GOOS,
shell: "bash", shell: "bash",
shellArg: "-c", shellArg: "-c",
escapedQuote: "\"", escapedQuote: "'",
openCommand: "open {{filename}}", openCommand: "open {{filename}}",
fallbackEscapedQuote: "\"",
} }
} }

View File

@ -265,6 +265,32 @@ func TestOSCommandQuote(t *testing.T) {
assert.EqualValues(t, expected, actual) assert.EqualValues(t, expected, actual)
} }
// TestOSCommandQuoteSingleQuote tests the quote function with ' quotes explicitly for Linux
func TestOSCommandQuoteSingleQuote(t *testing.T) {
osCommand := newDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote("hello 'test'")
expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteSingleQuote tests the quote function with " quotes explicitly for Linux
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
osCommand := newDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote(`hello "test"`)
expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote
assert.EqualValues(t, expected, actual)
}
func TestOSCommandUnquote(t *testing.T) { func TestOSCommandUnquote(t *testing.T) {
osCommand := newDummyOSCommand() osCommand := newDummyOSCommand()

View File

@ -2,9 +2,10 @@ package commands
func getPlatform() *Platform { func getPlatform() *Platform {
return &Platform{ return &Platform{
os: "windows", os: "windows",
shell: "cmd", shell: "cmd",
shellArg: "/c", shellArg: "/c",
escapedQuote: `\"`, escapedQuote: `\"`,
fallbackEscapedQuote: "\\'",
} }
} }

View File

@ -12,7 +12,7 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
if message == "" { if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr")) return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
} }
sub, err := gui.GitCommand.Commit(g, message) sub, err := gui.GitCommand.Commit(message)
if err != nil { if err != nil {
// TODO need to find a way to send through this error // TODO need to find a way to send through this error
if err != gui.Errors.ErrSubProcess { if err != gui.Errors.ErrSubProcess {