1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-09 07:24:03 +02:00

Merge branch 'master' into feature/auto-updates

This commit is contained in:
Jesse Duffield 2018-08-25 11:02:46 +10:00
commit f24c95aede
52 changed files with 441 additions and 121 deletions

@ -1,26 +1,42 @@
# Golang CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-go/ for more details
version: 2 version: 2
jobs: jobs:
build: build:
docker: docker:
# specify the version - image: circleci/golang:1.10
- image: circleci/golang:1.9
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/postgres:9.4
#### TEMPLATE_NOTE: go expects specific checkout path representing url
#### expecting it in the form of
#### /go/src/github.com/circleci/go-tool
#### /go/src/bitbucket.org/circleci/go-tool
working_directory: /go/src/github.com/jesseduffield/lazygit working_directory: /go/src/github.com/jesseduffield/lazygit
steps: steps:
- checkout - checkout
- run:
name: Run tests
command: |
./test.sh
- run:
name: Push on codecov result
command: |
bash <(curl -s https://codecov.io/bash)
# specify any bash command here prefixed with `run: ` release:
- run: go test -v ./... docker:
- run: bash <(curl -s https://codecov.io/bash) - image: circleci/golang:1.10
working_directory: /go/src/github.com/jesseduffield/lazygit
steps:
- checkout
- run:
name: Run gorelease
command: |
curl -sL https://git.io/goreleaser | bash
workflows:
version: 2
build:
jobs:
- build
release:
jobs:
- release:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/

@ -23,6 +23,7 @@ If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. Windows] - OS: [e.g. Windows]
- Lazygit Version [e.g. v0.1.45] - Lazygit Version [e.g. v0.1.45]
- The last commit id if you built project from sources (run : ```git-rev parse HEAD```)
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

3
.gitignore vendored

@ -13,6 +13,7 @@ TODO.md
# Tests # Tests
test/repos/repo test/repos/repo
coverage.txt
# Binaries # Binaries
lazygit lazygit

32
Gopkg.lock generated

@ -1,14 +1,6 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
name = "github.com/Sirupsen/logrus"
packages = ["."]
pruneopts = "NUT"
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
version = "v1.0.6"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:cd7ba2b29e93e2a8384e813dfc80ebb0f85d9214762e6ca89bb55a58092eab87" digest = "1:cd7ba2b29e93e2a8384e813dfc80ebb0f85d9214762e6ca89bb55a58092eab87"
@ -93,11 +85,11 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:c9a848b0484a72da2dae28957b4f67501fe27fa38bc73f4713e454353c0a4a60" digest = "1:f774b11ae458cae2d10b94ef66ef00ba1c57f1971dd0e5534ac743cbe574f6d4"
name = "github.com/jesseduffield/gocui" name = "github.com/jesseduffield/gocui"
packages = ["."] packages = ["."]
pruneopts = "NUT" pruneopts = "NUT"
revision = "432b7f6215f81ef1aaa1b2d9b69887822923cf79" revision = "7818a0f93387d1037cbd06f69323d9f8d068af7c"
[[projects]] [[projects]]
digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba" digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba"
@ -216,13 +208,20 @@
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
branch = "master"
digest = "1:41618aee8828e62dfe62d44f579c06892d0e98907d1c6d5bcd83bfe8536ec5a3" digest = "1:41618aee8828e62dfe62d44f579c06892d0e98907d1c6d5bcd83bfe8536ec5a3"
name = "github.com/shibukawa/configdir" name = "github.com/shibukawa/configdir"
packages = ["."] packages = ["."]
pruneopts = "NUT" pruneopts = "NUT"
revision = "e180dbdc8da04c4fa04272e875ce64949f38bd3e" revision = "e180dbdc8da04c4fa04272e875ce64949f38bd3e"
[[projects]]
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "NUT"
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
version = "v1.0.6"
[[projects]] [[projects]]
digest = "1:330e9062b308ac597e28485699c02223bd052437a6eed32a173c9227dcb9d95a" digest = "1:330e9062b308ac597e28485699c02223bd052437a6eed32a173c9227dcb9d95a"
name = "github.com/spf13/afero" name = "github.com/spf13/afero"
@ -266,6 +265,14 @@
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9" revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
version = "v1.1.0" version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:0e9a5ac14bcc11f205031a671b28c7e05cb88b2ebbe06f383c1ab0b2c12c7cb5"
name = "github.com/spkg/bom"
packages = ["."]
pruneopts = "NUT"
revision = "59b7046e48ad6bac800c5e1dd5142282cbfcf154"
[[projects]] [[projects]]
digest = "1:ccca1dcd18bc54e23b517a3c5babeff2e3924a7d8fc1932162225876cfe4bfb0" digest = "1:ccca1dcd18bc54e23b517a3c5babeff2e3924a7d8fc1932162225876cfe4bfb0"
name = "github.com/src-d/gcfg" name = "github.com/src-d/gcfg"
@ -447,7 +454,6 @@
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"github.com/Sirupsen/logrus",
"github.com/cloudfoundry/jibber_jabber", "github.com/cloudfoundry/jibber_jabber",
"github.com/davecgh/go-spew/spew", "github.com/davecgh/go-spew/spew",
"github.com/fatih/color", "github.com/fatih/color",
@ -456,7 +462,9 @@
"github.com/mgutz/str", "github.com/mgutz/str",
"github.com/nicksnyder/go-i18n/v2/i18n", "github.com/nicksnyder/go-i18n/v2/i18n",
"github.com/shibukawa/configdir", "github.com/shibukawa/configdir",
"github.com/sirupsen/logrus",
"github.com/spf13/viper", "github.com/spf13/viper",
"github.com/spkg/bom",
"github.com/stretchr/testify/assert", "github.com/stretchr/testify/assert",
"github.com/tcnksm/go-gitconfig", "github.com/tcnksm/go-gitconfig",
"golang.org/x/text/language", "golang.org/x/text/language",

@ -40,3 +40,7 @@
[[constraint]] [[constraint]]
name = "gopkg.in/src-d/go-git.v4" name = "gopkg.in/src-d/go-git.v4"
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f" revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
[[constraint]]
branch = "master"
name = "github.com/spkg/bom"

@ -1,4 +1,4 @@
# lazygit [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) # lazygit [![CircleCI](https://circleci.com/gh/jesseduffield/lazygit.svg?style=svg)](https://circleci.com/gh/jesseduffield/lazygit) [![codecov](https://codecov.io/gh/jesseduffield/lazygit/branch/master/graph/badge.svg)](https://codecov.io/gh/jesseduffield/lazygit) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)]()
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library. A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.

@ -37,3 +37,7 @@ The available attributes are:
- bold - bold
- reverse # useful for high-contrast - reverse # useful for high-contrast
- underline - underline
## Example Coloring:
![border example](/docs/resources/colored-border-example.png)

@ -44,6 +44,7 @@
<kbd>c</kbd>: checkout by name <kbd>c</kbd>: checkout by name
<kbd>n</kbd>: new branch <kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch <kbd>d</kbd>: delete branch
<kbd>D</kbd>: force delete branch
</pre> </pre>
## Commits Panel: ## Commits Panel:

Binary file not shown.

After

(image error) Size: 88 KiB

@ -5,7 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/gui"
@ -53,10 +53,7 @@ func NewApp(config config.AppConfigurer) (*App, error) {
return app, err return app, err
} }
app.Tr, err = i18n.NewLocalizer(app.Log) app.Tr = i18n.NewLocalizer(app.Log)
if err != nil {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand) app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand)
if err != nil { if err != nil {

@ -7,7 +7,7 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
gitconfig "github.com/tcnksm/go-gitconfig" gitconfig "github.com/tcnksm/go-gitconfig"
@ -223,8 +223,14 @@ func (c *GitCommand) NewBranch(name string) error {
} }
// DeleteBranch delete branch // DeleteBranch delete branch
func (c *GitCommand) DeleteBranch(branch string) error { func (c *GitCommand) DeleteBranch(branch string, force bool) error {
return c.OSCommand.RunCommand("git branch -d " + branch) var command string
if force {
command = "git branch -D "
} else {
command = "git branch -d "
}
return c.OSCommand.RunCommand(command + branch)
} }
// ListStash list stash // ListStash list stash

@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/jesseduffield/lazygit/pkg/test" "github.com/jesseduffield/lazygit/pkg/test"
) )

@ -11,7 +11,7 @@ import (
"github.com/mgutz/str" "github.com/mgutz/str"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig" gitconfig "github.com/tcnksm/go-gitconfig"
) )
@ -175,7 +175,8 @@ func (c *OSCommand) Unquote(message string) string {
return message return message
} }
func (C *OSCommand) AppendLineToFile(filename, line string) error { // AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil { if err != nil {
return err return err

@ -121,10 +121,7 @@ func LoadUserConfigFromFile(v *viper.Viper) error {
folder = configDirs.QueryFolderContainsFile("config.yml") folder = configDirs.QueryFolderContainsFile("config.yml")
} }
v.AddConfigPath(folder.Path) v.AddConfigPath(folder.Path)
if err := v.MergeInConfig(); err != nil { return v.MergeInConfig()
return err
}
return nil
} }
// InsertToUserConfig adds a key/value pair to the user's config and saves it // InsertToUserConfig adds a key/value pair to the user's config and saves it
@ -139,10 +136,7 @@ func (c *AppConfig) InsertToUserConfig(key, value string) error {
return err return err
} }
v.Set(key, value) v.Set(key, value)
if err := v.WriteConfig(); err != nil { return v.WriteConfig()
return err
}
return nil
} }
func getDefaultConfig() []byte { func getDefaultConfig() []byte {

@ -7,7 +7,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
"gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing"
) )

@ -62,20 +62,34 @@ func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, false)
}
func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, true)
}
func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
checkedOutBranch := gui.State.Branches[0] checkedOutBranch := gui.State.Branches[0]
selectedBranch := gui.getSelectedBranch(v) selectedBranch := gui.getSelectedBranch(v)
if checkedOutBranch.Name == selectedBranch.Name { if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch")) return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
} }
title := gui.Tr.SLocalize("DeleteBranch")
var messageId string
if force {
messageId = "ForceDeleteBranchMessage"
} else {
messageId = "DeleteBranchMessage"
}
message := gui.Tr.TemplateLocalize( message := gui.Tr.TemplateLocalize(
"DeleteBranchMessage", messageId,
Teml{ Teml{
"selectedBranchName": selectedBranch.Name, "selectedBranchName": selectedBranch.Name,
}, },
) )
title := gui.Tr.SLocalize("DeleteBranch")
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name); err != nil { if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
return gui.createErrorPanel(g, err.Error()) return gui.createErrorPanel(g, err.Error())
} }
return gui.refreshSidePanels(g) return gui.refreshSidePanels(g)
@ -108,6 +122,7 @@ func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error {
"c": gui.Tr.SLocalize("checkoutByName"), "c": gui.Tr.SLocalize("checkoutByName"),
"n": gui.Tr.SLocalize("newBranch"), "n": gui.Tr.SLocalize("newBranch"),
"d": gui.Tr.SLocalize("deleteBranch"), "d": gui.Tr.SLocalize("deleteBranch"),
"D": gui.Tr.SLocalize("forceDeleteBranch"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"), "← → ↑ ↓": gui.Tr.SLocalize("navigate"),
}) })
} }

@ -251,14 +251,6 @@ func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.EditFile) return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.EditFile)
} }
func (gui *Gui) openFile(filename string) error {
err := gui.OSCommand.OpenFile(filename)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return nil
}
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g) file, err := gui.getSelectedFile(g)
if err != nil { if err != nil {

@ -15,13 +15,13 @@ import (
// "strings" // "strings"
"github.com/Sirupsen/logrus"
"github.com/golang-collections/collections/stack" "github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates" "github.com/jesseduffield/lazygit/pkg/updates"
"github.com/sirupsen/logrus"
) )
// OverlappingEdges determines if panel edges overlap // OverlappingEdges determines if panel edges overlap
@ -152,14 +152,15 @@ func (gui *Gui) setAppStatus(status string) error {
func (gui *Gui) layout(g *gocui.Gui) error { func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true g.Highlight = true
width, height := g.Size() width, height := g.Size()
version := gui.Config.GetVersion()
leftSideWidth := width / 3 leftSideWidth := width / 3
statusFilesBoundary := 2 statusFilesBoundary := 2
filesBranchesBoundary := 2 * height / 5 // height - 20 filesBranchesBoundary := 2 * height / 5 // height - 20
commitsBranchesBoundary := 3 * height / 5 // height - 10 commitsBranchesBoundary := 3 * height / 5 // height - 10
commitsStashBoundary := height - 5 // height - 5 commitsStashBoundary := height - 5 // height - 5
optionsVersionBoundary := width - max(len(version), 1)
minimumHeight := 16 minimumHeight := 16
minimumWidth := 10 minimumWidth := 10
version := gui.Config.GetVersion()
appStatusView, _ := g.View("appStatus") appStatusView, _ := g.View("appStatus")
appStatusOptionsBoundary := -2 appStatusOptionsBoundary := -2
@ -244,7 +245,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.FgColor = gocui.ColorWhite v.FgColor = gocui.ColorWhite
} }
if v, err := g.SetView("options", appStatusOptionsBoundary-1, optionsTop, width-len(version)-2, optionsTop+2, 0); err != nil { if v, err := g.SetView("options", appStatusOptionsBoundary-1, optionsTop, optionsVersionBoundary-1, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
@ -281,7 +282,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.Frame = false v.Frame = false
} }
if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil { if v, err := g.SetView("version", optionsVersionBoundary-1, optionsTop, width, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }

@ -16,6 +16,7 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
bindings := []Binding{ bindings := []Binding{
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit}, {ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit},
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit}, {ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit},
{ViewName: "", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.quit},
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, {ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain}, {ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
{ViewName: "", Key: gocui.KeyCtrlU, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, {ViewName: "", Key: gocui.KeyCtrlU, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
@ -57,6 +58,7 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout}, {ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout},
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch}, {ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch},
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch}, {ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch},
{ViewName: "branches", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleForceDeleteBranch},
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge}, {ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge},
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown}, {ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown},
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit}, {ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit},

@ -7,6 +7,8 @@ import (
"time" "time"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spkg/bom"
) )
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"} var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
@ -224,7 +226,9 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
gui.Log.Info(s) gui.Log.Info(s)
} }
v.Clear() v.Clear()
fmt.Fprint(v, s) output := string(bom.Clean([]byte(s)))
output = utils.NormalizeLinefeeds(output)
fmt.Fprint(v, output)
v.Wrap = true v.Wrap = true
return nil return nil
}) })

@ -132,7 +132,10 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Verwijder branch", Other: "Verwijder branch",
}, &i18n.Message{ }, &i18n.Message{
ID: "DeleteBranchMessage", ID: "DeleteBranchMessage",
Other: "Weet je zeker dat je {{.selectedBranchName}} branch wil verwijderen?", Other: "Weet je zeker dat je branch {{.selectedBranchName}} wil verwijderen?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Weet je zeker dat je branch {{.selectedBranchName}} geforceerd wil verwijderen?",
}, &i18n.Message{ }, &i18n.Message{
ID: "CantMergeBranchIntoItself", ID: "CantMergeBranchIntoItself",
Other: "Je kan niet een branch in zichzelf mergen", Other: "Je kan niet een branch in zichzelf mergen",
@ -151,6 +154,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "deleteBranch", ID: "deleteBranch",
Other: "verwijder branch", Other: "verwijder branch",
}, &i18n.Message{
ID: "forceDeleteBranch",
Other: "verwijder branch (forceer)",
}, &i18n.Message{ }, &i18n.Message{
ID: "NoBranchesThisRepo", ID: "NoBranchesThisRepo",
Other: "Geen branches voor deze repo", Other: "Geen branches voor deze repo",

@ -140,7 +140,10 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "Delete Branch", Other: "Delete Branch",
}, &i18n.Message{ }, &i18n.Message{
ID: "DeleteBranchMessage", ID: "DeleteBranchMessage",
Other: "Are you sure you want delete the branch {{.selectedBranchName}} ?", Other: "Are you sure you want to delete the branch {{.selectedBranchName}}?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Are you sure you want to force delete the branch {{.selectedBranchName}}?",
}, &i18n.Message{ }, &i18n.Message{
ID: "CantMergeBranchIntoItself", ID: "CantMergeBranchIntoItself",
Other: "You cannot merge a branch into itself", Other: "You cannot merge a branch into itself",
@ -159,6 +162,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "deleteBranch", ID: "deleteBranch",
Other: "delete branch", Other: "delete branch",
}, &i18n.Message{
ID: "forceDeleteBranch",
Other: "delete branch (force)",
}, &i18n.Message{ }, &i18n.Message{
ID: "NoBranchesThisRepo", ID: "NoBranchesThisRepo",
Other: "No branches for this repo", Other: "No branches for this repo",

@ -1,7 +1,7 @@
package i18n package i18n
import ( import (
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/cloudfoundry/jibber_jabber" "github.com/cloudfoundry/jibber_jabber"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language" "golang.org/x/text/language"
@ -18,33 +18,12 @@ type Localizer struct {
} }
// NewLocalizer creates a new Localizer // NewLocalizer creates a new Localizer
func NewLocalizer(log *logrus.Logger) (*Localizer, error) { func NewLocalizer(log *logrus.Logger) *Localizer {
userLang := detectLanguage(jibber_jabber.DetectLanguage)
// detect the user's language
userLang, err := jibber_jabber.DetectLanguage()
if err != nil {
if err.Error() != "Could not detect Language" {
return nil, err
}
userLang = "C"
}
log.Info("language: " + userLang) log.Info("language: " + userLang)
// create a i18n bundle that can be used to add translations and other things return setupLocalizer(log, userLang)
i18nBundle := &i18n.Bundle{DefaultLanguage: language.English}
addBundles(log, i18nBundle)
// return the new localizer that can be used to translate text
i18nLocalizer := i18n.NewLocalizer(i18nBundle, userLang)
localizer := &Localizer{
i18nLocalizer: i18nLocalizer,
language: userLang,
Log: log,
}
return localizer, nil
} }
// Localize handels the translations // Localize handels the translations
@ -82,17 +61,42 @@ func (l *Localizer) GetLanguage() string {
// add translation file(s) // add translation file(s)
func addBundles(log *logrus.Logger, i18nBundle *i18n.Bundle) { func addBundles(log *logrus.Logger, i18nBundle *i18n.Bundle) {
err := addPolish(i18nBundle) fs := []func(*i18n.Bundle) error{
if err != nil { addPolish,
log.Fatal(err) addDutch,
} addEnglish,
err = addDutch(i18nBundle)
if err != nil {
log.Fatal(err)
}
err = addEnglish(i18nBundle)
if err != nil {
log.Fatal(err)
} }
for _, f := range fs {
if err := f(i18nBundle); err != nil {
log.Fatal(err)
}
}
}
// detectLanguage extracts user language from environment
func detectLanguage(langDetector func() (string, error)) string {
if userLang, err := langDetector(); err == nil {
return userLang
}
return "C"
}
// setupLocalizer creates a new localizer using given userLang
func setupLocalizer(log *logrus.Logger, userLang string) *Localizer {
// create a i18n bundle that can be used to add translations and other things
i18nBundle := &i18n.Bundle{DefaultLanguage: language.English}
addBundles(log, i18nBundle)
// return the new localizer that can be used to translate text
i18nLocalizer := i18n.NewLocalizer(i18nBundle, userLang)
return &Localizer{
i18nLocalizer: i18nLocalizer,
language: userLang,
Log: log,
}
} }

81
pkg/i18n/i18n_test.go Normal file

@ -0,0 +1,81 @@
package i18n
import (
"fmt"
"testing"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestNewLocalizer(t *testing.T) {
assert.NotNil(t, NewLocalizer(logrus.New()))
}
func TestDetectLanguage(t *testing.T) {
type scenario struct {
langDetector func() (string, error)
expected string
}
scenarios := []scenario{
{
func() (string, error) {
return "", fmt.Errorf("An error occurred")
},
"C",
},
{
func() (string, error) {
return "en", nil
},
"en",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, detectLanguage(s.langDetector))
}
}
func TestLocalizer(t *testing.T) {
type scenario struct {
userLang string
test func(*Localizer)
}
scenarios := []scenario{
{
"C",
func(l *Localizer) {
assert.EqualValues(t, "C", l.GetLanguage())
assert.Equal(t, "Diff", l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "DiffTitle",
},
}))
assert.Equal(t, "Diff", l.SLocalize("DiffTitle"))
assert.Equal(t, "Are you sure you want to delete the branch test?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
},
},
{
"nl",
func(l *Localizer) {
assert.EqualValues(t, "nl", l.GetLanguage())
assert.Equal(t, "Diff", l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "DiffTitle",
},
}))
assert.Equal(t, "Diff", l.SLocalize("DiffTitle"))
assert.Equal(t, "Weet je zeker dat je branch test wil verwijderen?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
},
},
}
for _, s := range scenarios {
s.test(setupLocalizer(logrus.New(), s.userLang))
}
}

@ -4,6 +4,7 @@ import (
"errors" "errors"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -11,15 +12,20 @@ import (
// GenerateRepo generates a repo from test/repos and changes the directory to be // GenerateRepo generates a repo from test/repos and changes the directory to be
// inside the newly made repo // inside the newly made repo
func GenerateRepo(filename string) error { func GenerateRepo(filename string) error {
testPath := utils.GetProjectRoot() + "/test/repos/" reposDir := "/test/repos/"
testPath := utils.GetProjectRoot() + reposDir
// workaround for debian packaging
if _, err := os.Stat(testPath); os.IsNotExist(err) {
cwd, _ := os.Getwd()
testPath = filepath.Dir(filepath.Dir(cwd)) + reposDir
}
if err := os.Chdir(testPath); err != nil { if err := os.Chdir(testPath); err != nil {
return err return err
} }
if output, err := exec.Command("bash", filename).CombinedOutput(); err != nil { if output, err := exec.Command("bash", filename).CombinedOutput(); err != nil {
return errors.New(string(output)) return errors.New(string(output))
} }
if err := os.Chdir(testPath + "repo"); err != nil {
return err return os.Chdir(testPath + "repo")
}
return nil
} }

@ -13,10 +13,10 @@ import (
"github.com/kardianos/osext" "github.com/kardianos/osext"
"github.com/Sirupsen/logrus"
getter "github.com/jesseduffield/go-getter" getter "github.com/jesseduffield/go-getter"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/sirupsen/logrus"
) )
// Update checks for updates and does updates // Update checks for updates and does updates

@ -64,6 +64,13 @@ func TrimTrailingNewline(str string) string {
return str return str
} }
// NormalizeLinefeeds - Removes all Windows and Mac style line feeds
func NormalizeLinefeeds(str string) string {
str = strings.Replace(str, "\r\n", "\n", -1)
str = strings.Replace(str, "\r", "", -1)
return str
}
// GetProjectRoot returns the path to the root of the project. Only to be used // GetProjectRoot returns the path to the root of the project. Only to be used
// in testing contexts, as with binaries it's unlikely this path will exist on // in testing contexts, as with binaries it's unlikely this path will exist on
// the machine // the machine

@ -81,3 +81,36 @@ func TestTrimTrailingNewline(t *testing.T) {
assert.EqualValues(t, s.expected, TrimTrailingNewline(s.str)) assert.EqualValues(t, s.expected, TrimTrailingNewline(s.str))
} }
} }
func TestNormalizeLinefeeds(t *testing.T) {
type scenario struct {
byteArray []byte
expected []byte
}
var scenarios = []scenario{
{
// \r\n
[]byte{97, 115, 100, 102, 13, 10},
[]byte{97, 115, 100, 102, 10},
},
{
// bash\r\nblah
[]byte{97, 115, 100, 102, 13, 10, 97, 115, 100, 102},
[]byte{97, 115, 100, 102, 10, 97, 115, 100, 102},
},
{
// \r
[]byte{97, 115, 100, 102, 13},
[]byte{97, 115, 100, 102},
},
{
// \n
[]byte{97, 115, 100, 102, 10},
[]byte{97, 115, 100, 102, 10},
},
}
for _, s := range scenarios {
assert.EqualValues(t, string(s.expected), NormalizeLinefeeds(string(s.byteArray)))
}
}

14
test.sh Executable file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e
echo "" > coverage.txt
for d in $( find ./* -maxdepth 10 ! -path "./vendor*" ! -path "./.git*" -type d); do
if ls $d/*.go &> /dev/null; then
go test -v -race -coverprofile=profile.out -covermode=atomic $d
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
fi
done

23
test/repos/bom.sh Executable file

@ -0,0 +1,23 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
cat <<EOT >> windowslf.txt
asdf
asdf
EOT
cat <<EOT >> linuxlf.txt
asdf
asdf
EOT
cat <<EOT >> bomtest.txt
A,B,C,D,E
F,G,H,I,J
K,L,M,N,O
P,Q,R,S,T
U,V,W,X,Y
Z,1,2,3,4
EOT

@ -653,17 +653,31 @@ func (g *Gui) onKey(ev *termbox.Event) error {
// execKeybindings executes the keybinding handlers that match the passed view // execKeybindings executes the keybinding handlers that match the passed view
// and event. The value of matched is true if there is a match and no errors. // and event. The value of matched is true if there is a match and no errors.
func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err error) { func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err error) {
matched = false var globalKb *keybinding
for _, kb := range g.keybindings { for _, kb := range g.keybindings {
if kb.handler == nil { if kb.handler == nil {
continue continue
} }
if kb.matchKeypress(Key(ev.Key), ev.Ch, Modifier(ev.Mod)) && kb.matchView(v) { if !kb.matchKeypress(Key(ev.Key), ev.Ch, Modifier(ev.Mod)) {
if err := kb.handler(g, v); err != nil { continue
return false, err }
} if kb.matchView(v) {
matched = true return g.execKeybinding(v, kb)
}
if kb.viewName == "" && (!v.Editable || kb.ch == 0) {
globalKb = kb
} }
} }
return matched, nil if globalKb != nil {
return g.execKeybinding(v, globalKb)
}
return false, nil
}
// execKeybinding executes a given keybinding
func (g *Gui) execKeybinding(v *View, kb *keybinding) (bool, error) {
if err := kb.handler(g, v); err != nil {
return false, err
}
return true, nil
} }

@ -38,9 +38,6 @@ func (kb *keybinding) matchView(v *View) bool {
if v.Editable == true && kb.ch != 0 { if v.Editable == true && kb.ch != 0 {
return false return false
} }
if kb.viewName == "" {
return true
}
return v != nil && kb.viewName == v.name return v != nil && kb.viewName == v.name
} }

21
vendor/github.com/spkg/bom/LICENSE.md generated vendored Normal file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 John Jeffery
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.

39
vendor/github.com/spkg/bom/bom.go generated vendored Normal file

@ -0,0 +1,39 @@
// Package bom is used to clean up UTF-8 Byte Order Marks.
package bom
import (
"bufio"
"io"
)
const (
bom0 = 0xef
bom1 = 0xbb
bom2 = 0xbf
)
// Clean returns b with the 3 byte BOM stripped off the front if it is present.
// If the BOM is not present, then b is returned.
func Clean(b []byte) []byte {
if len(b) >= 3 &&
b[0] == bom0 &&
b[1] == bom1 &&
b[2] == bom2 {
return b[3:]
}
return b
}
// NewReader returns an io.Reader that will skip over initial UTF-8 byte order marks.
func NewReader(r io.Reader) io.Reader {
buf := bufio.NewReader(r)
b, err := buf.Peek(3)
if err != nil {
// not enough bytes
return buf
}
if b[0] == bom0 && b[1] == bom1 && b[2] == bom2 {
discardBytes(buf, 3)
}
return buf
}

12
vendor/github.com/spkg/bom/discard_go14.go generated vendored Normal file

@ -0,0 +1,12 @@
// +build !go1.5
package bom
import "bufio"
func discardBytes(buf *bufio.Reader, n int) {
// cannot use the buf.Discard method as it was introduced in Go 1.5
for i := 0; i < n; i++ {
buf.ReadByte()
}
}

10
vendor/github.com/spkg/bom/discard_go15.go generated vendored Normal file

@ -0,0 +1,10 @@
// +build go1.5
package bom
import "bufio"
func discardBytes(buf *bufio.Reader, n int) {
// the Discard method was introduced in Go 1.5
buf.Discard(n)
}