1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-15 01:34:26 +02:00

Merge branch 'master' into feature/help

conflicts resolved
This commit is contained in:
Dawid Dziurla
2018-09-09 10:39:08 +02:00
26 changed files with 528 additions and 140 deletions

View File

@ -2,14 +2,23 @@ version: 2
jobs: jobs:
build: build:
docker: docker:
- image: circleci/golang:1.10 - image: circleci/golang:1.11
working_directory: /go/src/github.com/jesseduffield/lazygit working_directory: /go/src/github.com/jesseduffield/lazygit
steps: steps:
- checkout - checkout
- restore_cache: - run:
keys: name: Ensure go.mod file is up to date
- pkg-cache-{{ checksum "Gopkg.lock" }} command: |
export GO111MODULE=on
mv go.mod /tmp/
go mod init
export GO111MODULE=auto
if [ $(diff /tmp/go.mod go.mod|wc -l) -gt 0 ]; then
diff /tmp/go.mod go.mod
exit 1;
fi
- run: - run:
name: Run gofmt -s name: Run gofmt -s
command: | command: |
@ -17,6 +26,9 @@ jobs:
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \; find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1; exit 1;
fi fi
- restore_cache:
keys:
- pkg-cache-{{ checksum "Gopkg.lock" }}-v2
- run: - run:
name: Run tests name: Run tests
command: | command: |
@ -31,9 +43,9 @@ jobs:
command: | command: |
bash <(curl -s https://codecov.io/bash) bash <(curl -s https://codecov.io/bash)
- save_cache: - save_cache:
key: pkg-cache-{{ checksum "Gopkg.lock" }} key: pkg-cache-{{ checksum "Gopkg.lock" }}-v2
paths: paths:
- "/go/pkg" - ~/.cache/go-build
release: release:
docker: docker:

View File

@ -15,9 +15,10 @@ welcome your pull requests:
1. Fork the repo and create your branch from `master`. 1. Fork the repo and create your branch from `master`.
2. If you've added code that should be tested, add tests. 2. If you've added code that should be tested, add tests.
3. If you've added code that need documentation, update the documentation. 3. If you've added code that need documentation, update the documentation.
4. Be sure to test your modifications. 4. Make sure your code follows the [effective go](https://golang.org/doc/effective_go.html) guidelines as much as possible.
5. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 5. Be sure to test your modifications.
6. Issue that pull request! 6. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
7. Issue that pull request!
## Code of conduct ## Code of conduct
Please note by participating in this project, you agree to abide by the [code of conduct]. Please note by participating in this project, you agree to abide by the [code of conduct].

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
# run with:
# docker build -t lazygit .
# docker run -it lazygit:latest
FROM golang:alpine
RUN apk add -U git xdg-utils
ADD . /go/src/github.com/jesseduffield/lazygit
RUN go install github.com/jesseduffield/lazygit
WORKDIR /go/src/github.com/jesseduffield/lazygit

5
Gopkg.lock generated
View File

@ -189,11 +189,11 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:acbcdae312c37a8019e0f573a9be26499058d5e1244243655373d2fd97714658" digest = "1:71e6c15797951d3fabaa944d70253e36a6cee96bf54ca0bc43ca3de3b4814bbb"
name = "github.com/jesseduffield/gocui" name = "github.com/jesseduffield/gocui"
packages = ["."] packages = ["."]
pruneopts = "NUT" pruneopts = "NUT"
revision = "5d9e836837237cb3aadca51ecb37c7cf3345bfa4" revision = "2cb6e95bbbf850bb32cc1799e07d08ff0f144746"
[[projects]] [[projects]]
digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc" digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc"
@ -611,7 +611,6 @@
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"github.com/cloudfoundry/jibber_jabber", "github.com/cloudfoundry/jibber_jabber",
"github.com/davecgh/go-spew/spew",
"github.com/fatih/color", "github.com/fatih/color",
"github.com/golang-collections/collections/stack", "github.com/golang-collections/collections/stack",
"github.com/heroku/rollrus", "github.com/heroku/rollrus",

View File

@ -131,3 +131,7 @@ feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[
If you want to see what I (Jesse) am up to in terms of development, follow me on If you want to see what I (Jesse) am up to in terms of development, follow me on
[twitter](https://twitter.com/DuffieldJesse) or watch me program on [twitter](https://twitter.com/DuffieldJesse) or watch me program on
[twitch](https://www.twitch.tv/jesseduffield). [twitch](https://www.twitch.tv/jesseduffield).
## Alternatives
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
- [tig](https://github.com/jonas/tig)

View File

@ -14,10 +14,13 @@
- white - white
optionsTextColor: optionsTextColor:
- blue - blue
commitLength:
show: true
update: update:
method: prompt # can be: prompt | background | never method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined' reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
``` ```
## Platform Defaults: ## Platform Defaults:
@ -33,7 +36,7 @@
``` ```
os: os:
openCommand: 'bash -c \"xdg-open {{filename}} &>/dev/null &\"' openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
``` ```
### OSX: ### OSX:

62
go.mod Normal file
View File

@ -0,0 +1,62 @@
module github.com/jesseduffield/lazygit
require (
github.com/aws/aws-sdk-go v1.15.21
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/davecgh/go-spew v1.1.0
github.com/emirpasic/gods v1.9.0
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/go-ini/ini v1.38.2
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc
github.com/hashicorp/go-version v1.0.0
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce
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-20180905104005-2cb6e95bbbf8
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55
github.com/magiconair/properties v1.8.0
github.com/mattn/go-colorable v0.0.9
github.com/mattn/go-isatty v0.0.3
github.com/mattn/go-runewidth v0.0.2
github.com/mgutz/str v1.2.0
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699
github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb
github.com/pelletier/go-buffruneio v0.2.0
github.com/pelletier/go-toml v1.2.0
github.com/pkg/errors v0.8.0
github.com/pmezard/go-difflib v1.0.0
github.com/sergi/go-diff v1.0.0
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
github.com/sirupsen/logrus v1.0.6
github.com/spf13/afero v1.1.1
github.com/spf13/cast v1.2.0
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834
github.com/spf13/pflag v1.0.2
github.com/spf13/viper v1.1.0
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/src-d/gcfg v1.3.0
github.com/stretchr/testify v1.2.2
github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea
github.com/tcnksm/go-gitconfig v0.1.2
github.com/ulikunitz/xz v0.5.4
github.com/xanzy/ssh-agent v0.2.0
golang.org/x/crypto v0.0.0-20180808211826-de0752318171
golang.org/x/net v0.0.0-20180811021610-c39426892332
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0
golang.org/x/text v0.3.0
gopkg.in/src-d/go-billy.v4 v4.2.0
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714
gopkg.in/warnings.v0 v0.1.2
gopkg.in/yaml.v2 v2.2.1
)

View File

@ -43,11 +43,11 @@ func main() {
panic(err) panic(err)
} }
app, err := app.NewApp(appConfig) app, err := app.Setup(appConfig)
if err != nil { if err != nil {
app.Log.Error(err.Error()) app.Log.Error(err.Error())
panic(err) panic(err)
} }
app.GitCommand.SetupGit()
app.Gui.RunWithSubprocesses() app.Gui.RunWithSubprocesses()
} }

View File

@ -65,8 +65,8 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
}) })
} }
// NewApp retruns a new applications // Setup bootstrap a new application
func NewApp(config config.AppConfigurer) (*App, error) { func Setup(config config.AppConfigurer) (*App, error) {
app := &App{ app := &App{
closers: []io.Closer{}, closers: []io.Closer{},
Config: config, Config: config,

View File

@ -15,6 +15,48 @@ import (
gogit "gopkg.in/src-d/go-git.v4" gogit "gopkg.in/src-d/go-git.v4"
) )
func verifyInGitRepo(runCmd func(string) error) error {
return runCmd("git status")
}
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
for {
f, err := stat(".git")
if err == nil && f.IsDir() {
return nil
}
if !os.IsNotExist(err) {
return err
}
if err = chdir(".."); err != nil {
return err
}
}
}
func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repository, error), sLocalize func(string) string) (repository *gogit.Repository, worktree *gogit.Worktree, err error) {
repository, err = openGitRepository(".")
if err != nil {
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
return nil, nil, errors.New(sLocalize("GitconfigParseErr"))
}
return
}
worktree, err = repository.Worktree()
if err != nil {
return
}
return
}
// GitCommand is our main git interface // GitCommand is our main git interface
type GitCommand struct { type GitCommand struct {
Log *logrus.Entry Log *logrus.Entry
@ -26,22 +68,36 @@ type GitCommand struct {
// NewGitCommand it runs git commands // NewGitCommand it runs git commands
func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer) (*GitCommand, error) { func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer) (*GitCommand, error) {
gitCommand := &GitCommand{ var worktree *gogit.Worktree
var repo *gogit.Repository
fs := []func() error{
func() error {
return verifyInGitRepo(osCommand.RunCommand)
},
func() error {
return navigateToRepoRootDirectory(os.Stat, os.Chdir)
},
func() error {
var err error
repo, worktree, err = setupRepositoryAndWorktree(gogit.PlainOpen, tr.SLocalize)
return err
},
}
for _, f := range fs {
if err := f(); err != nil {
return nil, err
}
}
return &GitCommand{
Log: log, Log: log,
OSCommand: osCommand, OSCommand: osCommand,
Tr: tr, Tr: tr,
} Worktree: worktree,
return gitCommand, nil Repo: repo,
} }, nil
// SetupGit sets git repo up
func (c *GitCommand) SetupGit() {
c.verifyInGitRepo()
c.navigateToRepoRootDirectory()
if err := c.setupWorktree(); err != nil {
c.Log.Error(err)
panic(err)
}
} }
// GetStashEntries stash entryies // GetStashEntries stash entryies
@ -145,46 +201,11 @@ func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
return append(headResults, tailResults...) return append(headResults, tailResults...)
} }
func (c *GitCommand) verifyInGitRepo() {
if output, err := c.OSCommand.RunCommandWithOutput("git status"); err != nil {
fmt.Println(output)
os.Exit(1)
}
}
// GetBranchName branch name // GetBranchName branch name
func (c *GitCommand) GetBranchName() (string, error) { func (c *GitCommand) GetBranchName() (string, error) {
return c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD") return c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
} }
func (c *GitCommand) navigateToRepoRootDirectory() {
_, err := os.Stat(".git")
for os.IsNotExist(err) {
c.Log.Debug("going up a directory to find the root")
os.Chdir("..")
_, err = os.Stat(".git")
}
}
func (c *GitCommand) setupWorktree() error {
r, err := gogit.PlainOpen(".")
if err != nil {
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
errorMessage := c.Tr.SLocalize("GitconfigParseErr")
return errors.New(errorMessage)
}
return err
}
c.Repo = r
w, err := r.Worktree()
if err != nil {
return err
}
c.Worktree = w
return nil
}
// ResetHard does the equivalent of `git reset --hard HEAD` // ResetHard does the equivalent of `git reset --hard HEAD`
func (c *GitCommand) ResetHard() error { func (c *GitCommand) ResetHard() error {
return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset}) return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset})
@ -434,15 +455,6 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branchName) return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branchName)
} }
// Map (from https://gobyexample.com/collection-functions)
func Map(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}
func includesString(list []string, a string) bool { func includesString(list []string, a string) bool {
for _, b := range list { for _, b := range list {
if b == a { if b == a {

View File

@ -1,15 +1,53 @@
package commands package commands
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os"
"os/exec" "os/exec"
"testing" "testing"
"time"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test" "github.com/jesseduffield/lazygit/pkg/test"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
gogit "gopkg.in/src-d/go-git.v4"
) )
type fileInfoMock struct {
name string
size int64
fileMode os.FileMode
fileModTime time.Time
isDir bool
sys interface{}
}
func (f fileInfoMock) Name() string {
return f.name
}
func (f fileInfoMock) Size() int64 {
return f.size
}
func (f fileInfoMock) Mode() os.FileMode {
return f.fileMode
}
func (f fileInfoMock) ModTime() time.Time {
return f.fileModTime
}
func (f fileInfoMock) IsDir() bool {
return f.isDir
}
func (f fileInfoMock) Sys() interface{} {
return f.sys
}
func newDummyLog() *logrus.Entry { func newDummyLog() *logrus.Entry {
log := logrus.New() log := logrus.New()
log.Out = ioutil.Discard log.Out = ioutil.Discard
@ -20,6 +58,201 @@ func newDummyGitCommand() *GitCommand {
return &GitCommand{ return &GitCommand{
Log: newDummyLog(), Log: newDummyLog(),
OSCommand: newDummyOSCommand(), OSCommand: newDummyOSCommand(),
Tr: i18n.NewLocalizer(newDummyLog()),
}
}
func TestVerifyInGitRepo(t *testing.T) {
type scenario struct {
runCmd func(string) error
test func(error)
}
scenarios := []scenario{
{
func(string) error {
return nil
},
func(err error) {
assert.NoError(t, err)
},
},
{
func(string) error {
return fmt.Errorf("fatal: Not a git repository (or any of the parent directories): .git")
},
func(err error) {
assert.Error(t, err)
assert.Regexp(t, "fatal: .ot a git repository \\(or any of the parent directories\\): \\.git", err.Error())
},
},
}
for _, s := range scenarios {
s.test(verifyInGitRepo(s.runCmd))
}
}
func TestNavigateToRepoRootDirectory(t *testing.T) {
type scenario struct {
stat func(string) (os.FileInfo, error)
chdir func(string) error
test func(error)
}
scenarios := []scenario{
{
func(string) (os.FileInfo, error) {
return fileInfoMock{isDir: true}, nil
},
func(string) error {
return nil
},
func(err error) {
assert.NoError(t, err)
},
},
{
func(string) (os.FileInfo, error) {
return nil, fmt.Errorf("An error occurred")
},
func(string) error {
return nil
},
func(err error) {
assert.Error(t, err)
assert.EqualError(t, err, "An error occurred")
},
},
{
func(string) (os.FileInfo, error) {
return nil, os.ErrNotExist
},
func(string) error {
return fmt.Errorf("An error occurred")
},
func(err error) {
assert.Error(t, err)
assert.EqualError(t, err, "An error occurred")
},
},
{
func(string) (os.FileInfo, error) {
return nil, os.ErrNotExist
},
func(string) error {
return fmt.Errorf("An error occurred")
},
func(err error) {
assert.Error(t, err)
assert.EqualError(t, err, "An error occurred")
},
},
}
for _, s := range scenarios {
s.test(navigateToRepoRootDirectory(s.stat, s.chdir))
}
}
func TestSetupRepositoryAndWorktree(t *testing.T) {
type scenario struct {
openGitRepository func(string) (*gogit.Repository, error)
sLocalize func(string) string
test func(*gogit.Repository, *gogit.Worktree, error)
}
scenarios := []scenario{
{
func(string) (*gogit.Repository, error) {
return nil, fmt.Errorf(`unquoted '\' must be followed by new line`)
},
func(string) string {
return "error translated"
},
func(r *gogit.Repository, w *gogit.Worktree, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "error translated")
},
},
{
func(string) (*gogit.Repository, error) {
return nil, fmt.Errorf("Error from inside gogit")
},
func(string) string { return "" },
func(r *gogit.Repository, w *gogit.Worktree, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "Error from inside gogit")
},
},
{
func(string) (*gogit.Repository, error) {
return &gogit.Repository{}, nil
},
func(string) string { return "" },
func(r *gogit.Repository, w *gogit.Worktree, err error) {
assert.Error(t, err)
assert.Equal(t, gogit.ErrIsBareRepository, err)
},
},
{
func(string) (*gogit.Repository, error) {
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
r, err := gogit.PlainInit("/tmp/lazygit-test", false)
assert.NoError(t, err)
return r, nil
},
func(string) string { return "" },
func(r *gogit.Repository, w *gogit.Worktree, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s.test(setupRepositoryAndWorktree(s.openGitRepository, s.sLocalize))
}
}
func TestNewGitCommand(t *testing.T) {
actual, err := os.Getwd()
assert.NoError(t, err)
defer func() {
assert.NoError(t, os.Chdir(actual))
}()
type scenario struct {
setup func()
test func(*GitCommand, error)
}
scenarios := []scenario{
{
func() {
assert.NoError(t, os.Chdir("/tmp"))
},
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())
},
},
{
func() {
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
_, err := gogit.PlainInit("/tmp/lazygit-test", false)
assert.NoError(t, err)
assert.NoError(t, os.Chdir("/tmp/lazygit-test"))
},
func(gitCmd *GitCommand, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s.setup()
s.test(NewGitCommand(newDummyLog(), newDummyOSCommand(), i18n.NewLocalizer(newDummyLog())))
} }
} }

View File

@ -46,7 +46,7 @@ func TestOSCommandRunCommandWithOutput(t *testing.T) {
{ {
"rmdir unexisting-folder", "rmdir unexisting-folder",
func(output string, err error) { func(output string, err error) {
assert.Regexp(t, "rmdir: .* 'unexisting-folder': .*", err.Error()) assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
}, },
}, },
} }
@ -66,7 +66,7 @@ func TestOSCommandRunCommand(t *testing.T) {
{ {
"rmdir unexisting-folder", "rmdir unexisting-folder",
func(err error) { func(err error) {
assert.Regexp(t, "rmdir: .* 'unexisting-folder': .*", err.Error()) assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
}, },
}, },
} }

View File

@ -222,10 +222,13 @@ func GetDefaultConfig() []byte {
- white - white
optionsTextColor: optionsTextColor:
- blue - blue
commitLength:
show: true
update: update:
method: prompt # can be: prompt | background | never method: prompt # can be: prompt | background | never
days: 14 # how often a update is checked for days: 14 # how often a update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined' reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
`) `)
} }

View File

@ -4,5 +4,5 @@ package config
func GetPlatformDefaultConfig() []byte { func GetPlatformDefaultConfig() []byte {
return []byte( return []byte(
`os: `os:
openCommand: 'bash -c \"xdg-open {{filename}} &>/dev/null &\"'`) openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'`)
} }

View File

@ -1,6 +1,9 @@
package gui package gui
import ( import (
"strconv"
"strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
) )
@ -33,20 +36,6 @@ func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
return gui.switchFocus(g, v, gui.getFilesView(g)) return gui.switchFocus(g, v, gui.getFilesView(g))
} }
func (gui *Gui) handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
// resising ahead of time so that the top line doesn't get hidden to make
// room for the cursor on the second line
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.EditNewLine()
return nil
}
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
message := gui.Tr.TemplateLocalize( message := gui.Tr.TemplateLocalize(
"CloseConfirm", "CloseConfirm",
@ -57,3 +46,42 @@ func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
) )
return gui.renderString(g, "options", message) return gui.renderString(g, "options", message)
} }
func (gui *Gui) simpleEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == gocui.KeyTab:
v.EditNewLine()
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
default:
v.EditWrite(ch)
}
gui.RenderCommitLength()
}
func (gui *Gui) getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
}
func (gui *Gui) RenderCommitLength() {
if !gui.Config.GetUserConfig().GetBool("gui.commitLength.show") {
return
}
v := gui.getCommitMessageView(gui.g)
v.Subtitle = gui.getBufferLength(v)
}

View File

@ -11,7 +11,6 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error { func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
@ -116,20 +115,6 @@ func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, t
return nil return nil
} }
func (gui *Gui) handleNewline(g *gocui.Gui, v *gocui.View) error {
// resising ahead of time so that the top line doesn't get hidden to make
// room for the cursor on the second line
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
if _, err := g.SetView("confirmation", x0, y0, x1, y1+1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.EditNewLine()
return nil
}
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
actions := gui.Tr.TemplateLocalize( actions := gui.Tr.TemplateLocalize(
"CloseConfirm", "CloseConfirm",
@ -144,9 +129,6 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil { if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
return err return err
} }
if err := g.SetKeybinding("confirmation", gocui.KeyTab, gocui.ModNone, gui.handleNewline); err != nil {
return err
}
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose)) return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose))
} }
@ -161,17 +143,3 @@ func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
coloredMessage := colorFunction(strings.TrimSpace(message)) coloredMessage := colorFunction(strings.TrimSpace(message))
return gui.createConfirmationPanel(g, currentView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil) return gui.createConfirmationPanel(g, currentView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
} }
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 := utils.TrimTrailingNewline(v.Buffer())
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}

View File

@ -202,6 +202,7 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
g.Update(func(g *gocui.Gui) error { g.Update(func(g *gocui.Gui) error {
g.SetViewOnTop("commitMessage") g.SetViewOnTop("commitMessage")
gui.switchFocus(g, filesView, commitMessageView) gui.switchFocus(g, filesView, commitMessageView)
gui.RenderCommitLength()
return nil return nil
}) })
return nil return nil

View File

@ -263,6 +263,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage") commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
commitMessageView.FgColor = gocui.ColorWhite commitMessageView.FgColor = gocui.ColorWhite
commitMessageView.Editable = true commitMessageView.Editable = true
commitMessageView.Editor = gocui.EditorFunc(gui.simpleEditor)
} }
} }
@ -308,9 +309,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
} }
} }
gui.resizePopupPanels(g) return gui.resizeCurrentPopupPanel(g)
return nil
} }
func (gui *Gui) promptAnonymousReporting() error { func (gui *Gui) promptAnonymousReporting() error {
@ -365,14 +364,6 @@ func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*goc
}() }()
} }
func (gui *Gui) resizePopupPanels(g *gocui.Gui) error {
v := g.CurrentView()
if v.Name() == "commitMessage" || v.Name() == "confirmation" {
return gui.resizePopupPanel(g, v)
}
return nil
}
// Run setup the gui with keybindings and start the mainloop // Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error { func (gui *Gui) Run() error {
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges) g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
@ -430,5 +421,10 @@ func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
if gui.State.Updating { if gui.State.Updating {
return gui.createUpdateQuitConfirmation(g, v) return gui.createUpdateQuitConfirmation(g, v)
} }
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
return gui.createConfirmationPanel(g, v, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}, nil)
}
return gocui.ErrQuit return gocui.ErrQuit
} }

View File

@ -339,11 +339,6 @@ func (gui *Gui) GetKeybindings() []Binding {
Key: gocui.KeyEsc, Key: gocui.KeyEsc,
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleCommitClose, Handler: gui.handleCommitClose,
}, {
ViewName: "commitMessage",
Key: gocui.KeyTab,
Modifier: gocui.ModNone,
Handler: gui.handleNewlineCommitMessage,
}, { }, {
ViewName: "menu", ViewName: "menu",
Key: gocui.KeyEsc, Key: gocui.KeyEsc,

View File

@ -277,3 +277,25 @@ func (gui *Gui) currentViewName(g *gocui.Gui) string {
currentView := g.CurrentView() currentView := g.CurrentView()
return currentView.Name() return currentView.Name()
} }
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
v := g.CurrentView()
if v.Name() == "commitMessage" || v.Name() == "confirmation" {
return gui.resizePopupPanel(g, v)
}
return nil
}
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)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}

View File

@ -367,6 +367,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "mergeIntoCurrentBranch", ID: "mergeIntoCurrentBranch",
Other: `merge into currently checked out branch`, Other: `merge into currently checked out branch`,
}, &i18n.Message{
ID: "ConfirmQuit",
Other: `Are you sure you want to quit?`,
}, },
) )
} }

View File

@ -387,6 +387,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "mergeIntoCurrentBranch", ID: "mergeIntoCurrentBranch",
Other: `merge into currently checked out branch`, Other: `merge into currently checked out branch`,
}, &i18n.Message{
ID: "ConfirmQuit",
Other: `Are you sure you want to quit?`,
}, },
) )
} }

View File

@ -365,6 +365,9 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "mergeIntoCurrentBranch", ID: "mergeIntoCurrentBranch",
Other: `scal do obecnej gałęzi`, Other: `scal do obecnej gałęzi`,
}, &i18n.Message{
ID: "ConfirmQuit",
Other: `Na pewno chcesz wyjść z programu?`,
}, },
) )
} }

View File

@ -114,8 +114,8 @@ func (v *View) EditDelete(back bool) {
func (v *View) EditNewLine() { func (v *View) EditNewLine() {
v.breakLine(v.cx, v.cy) v.breakLine(v.cx, v.cy)
v.ox = 0 v.ox = 0
v.cy = v.cy + 1
v.cx = 0 v.cx = 0
v.MoveCursor(0, 1, true)
} }
// MoveCursor moves the cursor taking into account the width of the line/view, // MoveCursor moves the cursor taking into account the width of the line/view,

View File

@ -476,6 +476,11 @@ func (g *Gui) flush() error {
return err return err
} }
} }
if v.Subtitle != "" {
if err := g.drawSubtitle(v, fgColor, bgColor); err != nil {
return err
}
}
} }
if err := g.draw(v); err != nil { if err := g.draw(v); err != nil {
return err return err
@ -582,6 +587,25 @@ func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error {
return nil return nil
} }
// drawSubtitle draws the subtitle of the view.
func (g *Gui) drawSubtitle(v *View, fgColor, bgColor Attribute) error {
if v.y0 < 0 || v.y0 >= g.maxY {
return nil
}
start := v.x1 - 5 - len(v.Subtitle)
for i, ch := range v.Subtitle {
x := start + i
if x >= v.x1 {
break
}
if err := g.SetRune(x, v.y0, ch, fgColor, bgColor); err != nil {
return err
}
}
return nil
}
// draw manages the cursor and calls the draw function of a view. // draw manages the cursor and calls the draw function of a view.
func (g *Gui) draw(v *View) error { func (g *Gui) draw(v *View) error {
if g.Cursor { if g.Cursor {

View File

@ -77,6 +77,9 @@ type View struct {
// If Frame is true, Title allows to configure a title for the view. // If Frame is true, Title allows to configure a title for the view.
Title string Title string
// If Frame is true, Subtitle allows to configure a subtitle for the view.
Subtitle string
// If Mask is true, the View will display the mask instead of the real // If Mask is true, the View will display the mask instead of the real
// content // content
Mask rune Mask rune