Compare commits

...

78 Commits
v0.6 ... v0.7.1

Author SHA1 Message Date
Jesse Duffield
1184990e16 move updated keybindings file 2019-03-03 23:27:19 +11:00
Jesse Duffield
ac5088eee6 allow both enter and space to execute menu item 2019-03-03 23:18:28 +11:00
Jesse Duffield
e36899d5c5 prevent crashes when scrolling up 2019-03-03 23:08:07 +11:00
Jesse Duffield
403526bc50 bump go mod 2019-03-03 16:15:52 +11:00
Jesse Duffield
a5d27764cd support user configuring mouse events to be enabled 2019-03-03 16:15:20 +11:00
Jesse Duffield
43758cbb5f i18n for rebase loading states 2019-03-03 16:11:20 +11:00
Jesse Duffield
0079015102 distinguish between inline and non-inline merge conflicts 2019-03-03 15:58:01 +11:00
Jesse Duffield
7a2176f479 acknowledge 'DU' statuses as being merge conflicts 2019-03-03 15:48:16 +11:00
Jesse Duffield
e0bdfad63a don't crash if we have no lines to stage 2019-03-03 15:48:01 +11:00
Jesse Duffield
f07fc31f8b fixup layout issue that was causing crashes when the window was too small 2019-03-03 15:34:53 +11:00
Jesse Duffield
4bb577ab7d show loading status for rebasing events 2019-03-03 15:21:33 +11:00
Jesse Duffield
8305d8e72f hide donate button if mouse events are disabled 2019-03-03 15:21:20 +11:00
Jesse Duffield
f68166e858 bump gocui to stop polling events after closing the gui when switching to a subprocess 2019-03-03 14:20:25 +11:00
Jesse Duffield
8925b161a7 windows support for skipping the editor 2019-03-03 12:44:10 +11:00
Jesse Duffield
0a1298765c use sh intead of bash for the sake of testing on the docker image 2019-03-02 21:31:48 +11:00
Jesse Duffield
273678f081 fix issue where you couldn't rearrange commits while rebasing onto a branch 2019-03-02 21:31:48 +11:00
Jesse Duffield
790235f64b add another match on the error message to tell us we've encountered merge conflicts 2019-03-02 21:31:48 +11:00
Jesse Duffield
abc0f7f0aa copy lazygit directory into docker container 2019-03-02 21:31:48 +11:00
Jesse Duffield
ab81f27fc7 don't show stack trace if lazygit is started outside of a git repo 2019-03-02 21:31:48 +11:00
Jesse Duffield
dbb01b028d populate dutch and polish i18n files with new messages 2019-03-02 21:31:48 +11:00
Jesse Duffield
0c886eddfb Revert "remove old rebase code now that we're only ever interactively rebasing"
This reverts commit 1a19b1412d.
2019-03-02 20:00:26 +11:00
Jesse Duffield
399346c2ee disable mouse feature until its ready 2019-03-02 20:00:17 +11:00
Jesse Duffield
7a170bbccf extend cheatsheet generator to contain context based keybindings 2019-03-02 19:05:21 +11:00
Jesse Duffield Duffield
8c0ea8f45f mouse support 2019-03-02 17:49:30 +11:00
Jesse Duffield
afbc028ad6 revert to the old keybinding for stash: I don't want anybody accidentally deleting changes they are trying to stash 2019-03-02 17:46:56 +11:00
Jesse Duffield
e331dfcaf8 update i18n 2019-03-02 17:46:56 +11:00
Jesse Duffield
1337f6e76a appease golangci 2019-03-02 17:45:53 +11:00
Jesse Duffield
4de31da4be fix up tests
This fixes up some git and oscommand tests, and pulls some tests into commit_list_builder_test.go

I've also made the NewDummyBlah functions public so that I didn't need to duplicate them across packages
I've also given OSCommand a SetCommand() method for setting the command on the struct
I've also created a file utils.go in the test package for creating convient 'CommandSwapper's, which
basically enable you to assert a sequence of commands on the command line, and swap each one out for
a different one to actually be executed
2019-03-02 13:39:09 +11:00
Jesse Duffield Duffield
23c51ba708 cleanup 2019-02-24 18:34:18 +11:00
Jesse Duffield Duffield
19a3ac603d improve script for making a test repo 2019-02-24 17:54:56 +11:00
Jesse Duffield Duffield
f4938deaae change type of cherryPickedCommits from []string to []*Commit 2019-02-24 17:34:19 +11:00
Jesse Duffield Duffield
639df512f3 decolorise strings before calculating padwidths 2019-02-24 17:05:17 +11:00
Jesse Duffield Duffield
a8858cbd12 support cherry picking commits 2019-02-24 13:51:52 +11:00
Jesse Duffield Duffield
1a19b1412d remove old rebase code now that we're only ever interactively rebasing 2019-02-24 11:03:14 +11:00
Jesse Duffield Duffield
95d451e59a Make it easier to run sync/async commands, switch to interactive rebase when rebasing on branches 2019-02-24 10:58:15 +11:00
Jesse Duffield
6c1d2d45ef some i18n and restricting rewording during interactive rebase 2019-02-24 09:42:35 +11:00
Jesse Duffield
f6b3a9b184 rearranging todo items while interactively rebasing 2019-02-24 09:42:34 +11:00
Jesse Duffield
cdc50e8557 more support for files with spaces 2019-02-24 09:42:34 +11:00
Jesse Duffield
0173fdb9df support file renames 2019-02-24 09:42:32 +11:00
Jesse Duffield
9661ea04f3 wrap amend command in a confirmation 2019-02-20 19:46:27 +11:00
Jesse Duffield
0228e25084 work towards more interactive rebase options 2019-02-19 23:36:36 +11:00
Jesse Duffield
935f774834 don't autostash when editing 2019-02-19 09:34:24 +11:00
Jesse Duffield
dcc7855fd0 pull commit list builder functions into their own builder struct 2019-02-19 09:18:30 +11:00
Jesse Duffield
a8e22ed82f show interactive rebase commits that are yet to go 2019-02-19 09:03:29 +11:00
Jesse Duffield
d44638130c add various interactive rebase commands 2019-02-18 23:27:54 +11:00
Jesse Duffield
76a27f417f rename any commit 2019-02-18 21:29:43 +11:00
Jesse Duffield
adc2529019 dealing better with errors at the top level 2019-02-18 19:42:23 +11:00
Jesse Duffield
43ab7318d3 remove HasMergeConflicts struct instance variables 2019-02-18 19:28:02 +11:00
Jesse Duffield
cb372d469f fix golangci errors 2019-02-16 21:30:29 +11:00
Jesse Duffield
88ba6efdd5 remove outdated TODO 2019-02-16 21:20:10 +11:00
Jesse Duffield
e011e9bc42 more work on rebasing feature 2019-02-16 21:01:17 +11:00
Jesse Duffield
ad93b4c863 consider whether the view has focus when rendering the contents of a view 2019-02-16 15:17:44 +11:00
Jesse Duffield
198cbee498 introduce panel contexts and more work on rebasing 2019-02-16 12:07:27 +11:00
Jesse Duffield
daca07eaca add loading panel 2019-02-16 12:03:22 +11:00
Jesse Duffield
34acaf7ac4 support users with gotest for coloured test output 2019-02-16 11:35:35 +11:00
Jesse Duffield
d967f65329 fix git tests 2019-02-16 11:24:47 +11:00
Jesse Duffield
306ac41fd8 bump gocui to support loader animations on views 2019-02-15 20:54:03 +11:00
Jesse Duffield
c101993405 post-merge cleanup 2019-02-11 22:47:14 +11:00
Jesse Duffield
6430ab6ac9 Merge branch 'master' into feature/rebasing 2019-02-11 22:46:27 +11:00
Jesse Duffield
e09f3905e9 update go.mod 2019-02-11 22:39:17 +11:00
Jesse Duffield
53e73313a2 bump gocui to version that uses go-errors as well 2019-02-11 22:39:17 +11:00
Jesse Duffield
0891797bf8 bump dep to include go-errors package 2019-02-11 22:39:17 +11:00
Jesse Duffield
cfe3605e6b use go-errors package to display stacktrace of errors that cause panics 2019-02-11 22:39:17 +11:00
Jesse Duffield
75ab8ec4d9 catch rebase errors and show in error panels 2019-02-11 21:29:47 +11:00
Jesse Duffield
77faf85cfc post-merge cleanup 2019-02-11 21:07:12 +11:00
Jesse Duffield
3d343e9b57 Merge branch 'master' into feature/rebasing 2019-02-11 21:02:53 +11:00
Jesse Duffield
a365615490 only use subprocess for merging, not rebasing 2018-12-11 22:16:48 +11:00
Jesse Duffield
9489a94473 Make merge panel its own panel 2018-12-11 22:02:12 +11:00
Jesse Duffield
e0ff46fe53 more work on rebasing including visual indicators 2018-12-11 09:39:54 +11:00
Glenn Vriesman
cce6f405a5 Making ci happier 2018-12-11 09:39:54 +11:00
Glenn Vriesman
e39d2ed44b Added check to invoke continue/refresh 2018-12-11 09:39:54 +11:00
Glenn Vriesman
7a7e885773 Added rebase support commands 2018-12-11 09:39:54 +11:00
Glenn Vriesman
34fd18a395 Error handling 2018-12-11 09:39:54 +11:00
Glenn Vriesman
a1ee11e54e Added error check to satisfy ci 2018-12-11 09:39:54 +11:00
Glenn Vriesman
7b850c56c4 Added some translations 2018-12-11 09:39:54 +11:00
Glenn Vriesman
88c01c1ded Added rebase keybinding 2018-12-11 09:39:54 +11:00
Glenn Vriesman
27994f7de8 Added rebase handler 2018-12-11 09:39:54 +11:00
Glenn Vriesman
670f0e37c7 Added rebase functions 2018-12-11 09:39:54 +11:00
66 changed files with 4397 additions and 1472 deletions

4
.gitignore vendored
View File

@@ -23,4 +23,6 @@ lazygit
!.gitignore
!.goreleaser.yml
!.circleci/
!.github/
!.github/
test/git_server/data

View File

@@ -10,5 +10,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o lazygit .
FROM alpine:latest
RUN apk add -U git xdg-utils
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
RUN echo "alias gg=lazygit" >> ~/.profile

13
Gopkg.lock generated
View File

@@ -96,6 +96,14 @@
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
digest = "1:ea1d5bfdb4ec5c2ee48c97865e6de1a28fa8c4849a3f56b27d521aa619038e06"
name = "github.com/go-errors/errors"
packages = ["."]
pruneopts = "NUT"
revision = "a6af135bd4e28680facf08a3d206b454abc877a4"
version = "v1.0.1"
[[projects]]
digest = "1:74d9b0a7b4107b41e0ade759fac64502876f82d29fb23d77b3dd24b194ee3dd5"
name = "github.com/go-ini/ini"
@@ -189,11 +197,11 @@
[[projects]]
branch = "master"
digest = "1:9b266d7748a5d94985fd9e323494f5b8ae1ab3e910418e898dfe7f03339ddbcd"
digest = "1:31a87f65dc451471f411d04742d2cb5ab79a699b8c73666b8fc29f47a8f43f7e"
name = "github.com/jesseduffield/gocui"
packages = ["."]
pruneopts = "NUT"
revision = "cfa9e452ba5ebf014041846851152d64a59dce14"
revision = "b502ee11d6743144c86226ca0366adaed727214d"
[[projects]]
branch = "master"
@@ -620,6 +628,7 @@
input-imports = [
"github.com/cloudfoundry/jibber_jabber",
"github.com/fatih/color",
"github.com/go-errors/errors",
"github.com/golang-collections/collections/stack",
"github.com/heroku/rollrus",
"github.com/jesseduffield/go-getter",

View File

@@ -17,6 +17,10 @@
- blue
commitLength:
show: true
git:
merging:
# only applicable to unix users
manualCommit: false
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for

View File

@@ -1,85 +0,0 @@
# Keybindings:
## Global:
<pre>
<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd>/<kbd>h</kbd><kbd>j</kbd><kbd>k</kbd><kbd>l</kbd>: navigate
<kbd>PgUp</kbd>/<kbd>PgDn</kbd> or <kbd>ctrl</kbd>+<kbd>u</kbd>/<kbd>ctrl</kbd>+<kbd>d</kbd>: scroll diff panel
(for <kbd>PgUp</kbd> and <kbd>PgDn</kbd>, use <kbd>fn</kbd>+<kbd>up</kbd>/<kbd>fn</kbd>+<kbd>down</kbd> on osx)
<kbd>q</kbd>: quit
<kbd>p</kbd>: pull
<kbd>shift</kbd>+<kbd>P</kbd>: push
</pre>
## Status Panel:
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
</pre>
## Files Panel:
<pre>
<kbd>space</kbd>: toggle staged
<kbd>a</kbd>: stage/unstage all
<kbd>c</kbd>: commit changes
<kbd>shift</kbd>+<kbd>C</kbd>: commit using git editor
<kbd>shift</kbd>+<kbd>S</kbd>: stash files
<kbd>t</kbd>: add patched (i.e. pick chunks of a file to add)
<kbd>o</kbd>: open
<kbd>e</kbd>: edit
<kbd>s</kbd>: open in sublime (requires 'subl' command)
<kbd>v</kbd>: open in vscode (requires 'code' command)
<kbd>i</kbd>: add to .gitignore
<kbd>d</kbd>: delete if untracked checkout if tracked (aka go away)
<kbd>shift</kbd>+<kbd>R</kbd>: refresh files
<kbd>shift</kbd>+<kbd>A</kbd>: abort merge
</pre>
## Branches Panel:
<pre>
<kbd>space</kbd>: checkout branch
<kbd>f</kbd>: force checkout branch
<kbd>m</kbd>: merge into currently checked out branch
<kbd>c</kbd>: checkout by name
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>D</kbd>: force delete branch
</pre>
## Commits Panel:
<pre>
<kbd>s</kbd>: squash down (only available for topmost commit)
<kbd>r</kbd>: rename commit
<kbd>shift</kbd>+<kbd>R</kbd>: rename commit using git editor
<kbd>g</kbd>: reset to this commit
</pre>
## Stash Panel:
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Popup Panel:
<pre>
<kbd>esc</kbd>: close/cancel
<kbd>enter</kbd>: confirm
<kbd>tab</kbd>: enter newline (if editing)
</pre>
## Resolving Merge Conflicts (Diff Panel):
<pre>
<kbd>←</kbd><kbd>→</kbd>/<kbd>h</kbd><kbd>l</kbd>: navigate conflicts
<kbd>↑</kbd><kbd>↓</kbd>/<kbd>k</kbd><kbd>j</kbd>: select hunk
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>z</kbd>: undo (only available while still inside diff panel)
</pre>

114
docs/Keybindings_en.md Normal file
View File

@@ -0,0 +1,114 @@
# Lazygit menu
## Global
<pre>
<kbd>m</kbd>: view merge/rebase options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
</pre>
## Status
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>s</kbd>: switch to a recent repo
</pre>
## Files
<pre>
<kbd>c</kbd>: commit changes
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: delete if untracked / checkout if tracked
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>i</kbd>: add to .gitignore
<kbd>r</kbd>: refresh files
<kbd>S</kbd>: stash files
<kbd>s</kbd>: soft reset to last commit
<kbd>a</kbd>: stage/unstage all
<kbd>t</kbd>: add patch
<kbd>D</kbd>: reset hard and remove untracked files
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>f</kbd>: fetch
</pre>
## Branches
<pre>
<kbd>space</kbd>: checkout
<kbd>o</kbd>: create pull request
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
</pre>
## Commits
<pre>
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>d</kbd>: delete commit
<kbd>J</kbd>: move commit down one
<kbd>K</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
</pre>
## Stash
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Main (Normal)
<pre>
<kbd>PgDn</kbd>: scroll down
<kbd>PgUp</kbd>: scroll up
</pre>
## Main (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: stage line
<kbd>a</kbd>: stage hunk
</pre>
## Main (Merging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>

3
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/emirpasic/gods v1.9.0
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.0.1
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
@@ -18,7 +19,7 @@ require (
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63
github.com/jesseduffield/gocui v0.0.0-20190115084758-cfa9e452ba5e
github.com/jesseduffield/gocui v0.0.0-20190303031804-b502ee11d674
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8

19
main.go
View File

@@ -8,6 +8,7 @@ import (
"path/filepath"
"runtime"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
)
@@ -39,16 +40,22 @@ func main() {
fmt.Printf("%s\n", config.GetDefaultConfig())
os.Exit(0)
}
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, *debuggingFlag)
if err != nil {
log.Fatal(err.Error())
}
app, err := app.Setup(appConfig)
if err != nil {
app.Log.Error(err.Error())
log.Fatal(err.Error())
app, err := app.NewApp(appConfig)
if err == nil {
err = app.Run()
}
app.Gui.RunWithSubprocesses()
if err != nil {
newErr := errors.Wrap(err, 0)
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.SLocalize("ErrorOccurred"), stackTrace))
}
}

View File

@@ -1,10 +1,12 @@
package app
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/heroku/rollrus"
"github.com/jesseduffield/lazygit/pkg/commands"
@@ -20,13 +22,14 @@ import (
type App struct {
closers []io.Closer
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *commands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.Localizer
Updater *updates.Updater // may only need this on the Gui
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *commands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.Localizer
Updater *updates.Updater // may only need this on the Gui
ClientContext string
}
func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
@@ -54,7 +57,7 @@ func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
environment := "production"
if config.GetDebug() {
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
environment = "development"
log = newDevelopmentLogger(config)
} else {
@@ -78,24 +81,34 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
})
}
// Setup bootstrap a new application
func Setup(config config.AppConfigurer) (*App, error) {
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.OSCommand = commands.NewOSCommand(app.Log, config)
app.Tr = i18n.NewLocalizer(app.Log)
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
if app.ClientContext != "" {
return app, nil
}
app.OSCommand = commands.NewOSCommand(app.Log, config)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
if err != nil {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr)
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
if err != nil {
if strings.Contains(err.Error(), "Not a git repository") {
fmt.Println("Not in a git repository. Use `git init` to create a new one")
os.Exit(1)
}
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
@@ -105,6 +118,39 @@ func Setup(config config.AppConfigurer) (*App, error) {
return app, nil
}
func (app *App) Run() error {
if app.ClientContext == "INTERACTIVE_REBASE" {
return app.Rebase()
}
if app.ClientContext == "EXIT_IMMEDIATELY" {
os.Exit(0)
}
return app.Gui.RunWithSubprocesses()
}
// Rebase contains logic for when we've been run in demon mode, meaning we've
// given lazygit as a command for git to call e.g. to edit a file
func (app *App) Rebase() error {
app.Log.Info("Lazygit invoked as interactive rebase demon")
app.Log.Info("args: ", os.Args)
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644); err != nil {
return err
}
} else if strings.HasSuffix(os.Args[1], ".git/COMMIT_EDITMSG") {
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
app.Log.Info("Lazygit demon did not match on any use cases")
}
return nil
}
// Close closes any resources
func (app *App) Close() error {
for _, closer := range app.closers {

View File

@@ -19,9 +19,9 @@ type Branch struct {
}
// GetDisplayStrings returns the dispaly string of branch
func (b *Branch) GetDisplayStrings() []string {
func (b *Branch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, b.GetColor())
if b.Selected && b.Pushables != "" && b.Pullables != "" {
if isFocused && b.Selected && b.Pushables != "" && b.Pullables != "" {
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
}

View File

@@ -2,30 +2,55 @@ package commands
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Commit : A git commit
type Commit struct {
Sha string
Name string
Pushed bool
Merged bool
Status string // one of "unpushed", "pushed", "merged", or "rebasing"
DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Copied bool // to know if this commit is ready to be cherry-picked somewhere
}
// GetDisplayStrings is a function.
func (c *Commit) GetDisplayStrings() []string {
func (c *Commit) GetDisplayStrings(isFocused bool) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgGreen)
green := color.New(color.FgYellow)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
white := color.New(color.FgWhite)
shaColor := yellow
if c.Pushed {
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
copied := color.New(color.FgCyan, color.BgBlue)
var shaColor *color.Color
switch c.Status {
case "unpushed":
shaColor = red
} else if !c.Merged {
case "pushed":
shaColor = yellow
case "merged":
shaColor = green
case "rebasing":
shaColor = blue
default:
shaColor = white
}
return []string{shaColor.Sprint(c.Sha), white.Sprint(c.Name)}
if c.Copied {
shaColor = copied
}
actionString := ""
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
}
return []string{shaColor.Sprint(c.Sha), actionString + white.Sprint(c.Name)}
}

58
pkg/commands/dummies.go Normal file
View File

@@ -0,0 +1,58 @@
package commands
import (
"io/ioutil"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
)
// This file exports dummy constructors for use by tests in other packages
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(NewDummyLog(), NewDummyAppConfig())
}
// NewDummyAppConfig creates a new dummy AppConfig for testing
func NewDummyAppConfig() *config.AppConfig {
appConfig := &config.AppConfig{
Name: "lazygit",
Version: "unversioned",
Commit: "",
BuildDate: "",
Debug: false,
BuildSource: "",
UserConfig: viper.New(),
}
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
return appConfig
}
// NewDummyLog creates a new dummy Log for testing
func NewDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
// NewDummyGitCommand creates a new dummy GitCommand for testing
func NewDummyGitCommand() *GitCommand {
return NewDummyGitCommandWithOSCommand(NewDummyOSCommand())
}
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *OSCommand) *GitCommand {
return &GitCommand{
Log: NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(NewDummyLog()),
Config: NewDummyAppConfig(),
getGlobalGitConfig: func(string) (string, error) { return "", nil },
getLocalGitConfig: func(string) (string, error) { return "", nil },
removeFile: func(string) error { return nil },
}
}

View File

@@ -5,12 +5,12 @@ package commands
import (
"bufio"
"bytes"
"errors"
"os"
"os/exec"
"strings"
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/jesseduffield/pty"
"github.com/mgutz/str"
)
@@ -21,7 +21,7 @@ import (
// NOTE: If the return data is empty it won't written anything to stdin
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
splitCmd := str.ToArgv(command)
cmd := exec.Command(splitCmd[0], splitCmd[1:]...)
cmd := c.command(splitCmd[0], splitCmd[1:]...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")

View File

@@ -5,18 +5,19 @@ import "github.com/fatih/color"
// File : A file from git status
// duplicating this for now
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
HasInlineMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
}
// GetDisplayStrings returns the display string of a file
func (f *File) GetDisplayStrings() []string {
func (f *File) GetDisplayStrings(isFocused bool) []string {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)

View File

@@ -1,12 +1,17 @@
package commands
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
"github.com/mgutz/str"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
@@ -27,11 +32,11 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f
}
if !os.IsNotExist(err) {
return err
return errors.Wrap(err, 0)
}
if err = chdir(".."); err != nil {
return err
return errors.Wrap(err, 0)
}
}
}
@@ -63,13 +68,14 @@ type GitCommand struct {
Worktree *gogit.Worktree
Repo *gogit.Repository
Tr *i18n.Localizer
Config config.AppConfigurer
getGlobalGitConfig func(string) (string, error)
getLocalGitConfig func(string) (string, error)
removeFile func(string) error
}
// 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, config config.AppConfigurer) (*GitCommand, error) {
var worktree *gogit.Worktree
var repo *gogit.Repository
@@ -99,6 +105,7 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer)
Tr: tr,
Worktree: worktree,
Repo: repo,
Config: config,
getGlobalGitConfig: gitconfig.Global,
getLocalGitConfig: gitconfig.Local,
removeFile: os.RemoveAll,
@@ -143,14 +150,15 @@ func (c *GitCommand) GetStatusFiles() []*File {
_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
file := &File{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: change == "UU",
Type: c.OSCommand.FileType(filename),
Name: filename,
DisplayString: statusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: change == "UU" || change == "AA" || change == "DU",
HasInlineMergeConflicts: change == "UU" || change == "AA",
Type: c.OSCommand.FileType(filename),
}
files = append(files, file)
}
@@ -240,26 +248,21 @@ func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch, a map is returned to ease look up
func (c *GitCommand) GetCommitsToPush() map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
if err != nil {
return pushables
}
for _, p := range utils.SplitLines(o) {
pushables[p] = true
}
return pushables
}
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)))
}
// RebaseBranch interactive rebases onto a branch
func (c *GitCommand) RebaseBranch(branchName string) error {
cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// Fetch fetch git repo
func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error {
return c.OSCommand.DetectUnamePass("git fetch", func(question string) string {
@@ -331,12 +334,18 @@ func (c *GitCommand) usingGpg() bool {
}
// Commit commits to git
func (c *GitCommand) Commit(message string, amend bool) (*exec.Cmd, error) {
amendParam := ""
if amend {
amendParam = " --amend"
func (c *GitCommand) Commit(message string) (*exec.Cmd, error) {
command := fmt.Sprintf("git commit -m %s", c.OSCommand.Quote(message))
if c.usingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
}
command := fmt.Sprintf("git commit%s -m %s", amendParam, c.OSCommand.Quote(message))
return nil, c.OSCommand.RunCommand(command)
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
command := "git commit --amend --no-edit"
if c.usingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
}
@@ -356,50 +365,10 @@ func (c *GitCommand) Push(branchName string, force bool, ask func(string) string
forceFlag = "--force-with-lease "
}
cmd := 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
// retaining the message of the higher commit
func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
// TODO: test this
if err := c.OSCommand.RunCommand("git reset --soft HEAD^"); err != nil {
return err
}
// TODO: if password is required, we need to return a subprocess
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,
// retaining the commit message of the lower commit
func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
commands := []string{
fmt.Sprintf("git checkout -q %s", shaValue),
fmt.Sprintf("git reset --soft %s^", shaValue),
fmt.Sprintf("git commit --amend -C %s^", shaValue),
fmt.Sprintf("git rebase --onto HEAD %s %s", shaValue, branchName),
}
for _, command := range commands {
c.Log.Info(command)
if output, err := c.OSCommand.RunCommandWithOutput(command); 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)
return errors.New(ret)
}
}
return nil
}
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName)))
@@ -426,7 +395,15 @@ func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
if tracked {
command = "git reset HEAD %s"
}
return c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(fileName)))
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(name))); err != nil {
return err
}
}
return nil
}
// GitStatus returns the plaintext short status of the repo
@@ -443,11 +420,30 @@ func (c *GitCommand) IsInMergeState() (bool, error) {
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
}
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (string, error) {
exists, err := c.OSCommand.FileExists(".git/rebase-apply")
if err != nil {
return "", err
}
if exists {
return "normal", nil
}
exists, err = c.OSCommand.FileExists(".git/rebase-merge")
if exists {
return "interactive", err
} else {
return "", err
}
}
// RemoveFile directly
func (c *GitCommand) RemoveFile(file *File) error {
// if the file isn't tracked, we assume you want to delete it
quotedFileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges {
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", file.Name)); err != nil {
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", quotedFileName)); err != nil {
return err
}
}
@@ -455,7 +451,7 @@ func (c *GitCommand) RemoveFile(file *File) error {
return c.removeFile(file.Name)
}
// if the file is tracked, we assume you want to just check it out
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", file.Name))
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", quotedFileName))
}
// Checkout checks out a branch, with --force if you set the force arg to true
@@ -470,7 +466,7 @@ func (c *GitCommand) Checkout(branch string, force bool) error {
// AddPatch prepares a subprocess for adding a patch by patch
// this will eventually be swapped out for a better solution inside the Gui
func (c *GitCommand) AddPatch(filename string) *exec.Cmd {
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", filename)
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", c.OSCommand.Quote(filename))
}
// PrepareCommitSubProcess prepares a subprocess for `git commit`
@@ -490,78 +486,6 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName))
}
func (c *GitCommand) getMergeBase() (string, error) {
currentBranch, err := c.CurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
if err != nil {
// swallowing error because it's not a big deal; probably because there are no commits yet
}
return output, nil
}
// GetCommits obtains the commits of the current branch
func (c *GitCommand) GetCommits() ([]*Commit, error) {
pushables := c.GetCommitsToPush()
log := c.GetLog()
lines := utils.SplitLines(log)
commits := make([]*Commit, len(lines))
// now we can split it up and turn it into commits
for i, line := range lines {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
_, pushed := pushables[sha]
commits[i] = &Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Pushed: pushed,
DisplayString: strings.Join(splitLine, " "),
}
}
return c.setCommitMergedStatuses(commits)
}
func (c *GitCommand) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
ancestor, err := c.getMergeBase()
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
commits[i].Merged = passedAncestor
}
return commits, nil
}
// GetLog gets the git log (currently limited to 30 commits for performance
// until we work out lazy loading
func (c *GitCommand) GetLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}
// Ignore adds a file to the gitignore for the repo
func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
@@ -569,7 +493,39 @@ func (c *GitCommand) Ignore(filename string) error {
// Show shows the diff of a commit
func (c *GitCommand) Show(sha string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
if err != nil {
return "", err
}
// if this is a merge commit, we need to go a step further and get the diff between the two branches we merged
revList, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git rev-list -1 --merges %s^...%s", sha, sha))
if err != nil {
// turns out we get an error here when it's the first commit. We'll just return the original show
return show, nil
}
if len(revList) == 0 {
return show, nil
}
// we want to pull out 1a6a69a and 3b51d7c from this:
// commit ccc771d8b13d5b0d4635db4463556366470fd4f6
// Merge: 1a6a69a 3b51d7c
lines := utils.SplitLines(show)
if len(lines) < 2 {
return show, nil
}
secondLineWords := strings.Split(lines[1], " ")
if len(secondLineWords) < 3 {
return show, nil
}
mergeDiff, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git diff --color %s...%s", secondLineWords[1], secondLineWords[2]))
if err != nil {
return "", err
}
return show + mergeDiff, nil
}
// GetRemoteURL returns current repo remote url
@@ -593,7 +549,8 @@ func (c *GitCommand) Diff(file *File, plain bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := "--color"
fileName := c.OSCommand.Quote(file.Name)
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
fileName := c.OSCommand.Quote(split[len(split)-1])
if file.HasStagedChanges && !file.HasUnstagedChanges {
cachedArg = "--cached"
}
@@ -620,10 +577,222 @@ func (c *GitCommand) ApplyPatch(patch string) (string, error) {
defer func() { _ = c.OSCommand.RemoveFile(filename) }()
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", filename))
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", c.OSCommand.Quote(filename)))
}
func (c *GitCommand) FastForward(branchName string) error {
upstream := "origin" // hardcoding for now
return c.OSCommand.RunCommand(fmt.Sprintf("git fetch %s %s:%s", upstream, branchName, branchName))
}
func (c *GitCommand) RunSkipEditorCommand(command string) error {
cmd := c.OSCommand.ExecutableFromString(command)
cmd.Env = append(
os.Environ(),
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"EDITOR="+c.OSCommand.GetLazygitPath(),
)
return c.OSCommand.RunExecutable(cmd)
}
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
// By default we skip the editor in the case where a commit will be made
func (c *GitCommand) GenericMerge(commandType string, command string) error {
return c.RunSkipEditorCommand(
fmt.Sprintf(
"git %s --%s",
commandType,
command,
),
)
}
func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) {
todo, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
if err != nil {
return nil, err
}
return c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, false)
}
func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error {
// we must ensure that we have at least two commits after the selected one
if len(commits) <= index+2 {
// assuming they aren't picking the bottom commit
return errors.New(c.Tr.SLocalize("NoRoom"))
}
todo := ""
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
for _, commit := range orderedCommits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action string) error {
todo, err := c.GenerateGenericRebaseTodo(commits, index, action)
if err != nil {
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
ex := c.OSCommand.GetLazygitPath()
debug := "FALSE"
if c.OSCommand.Config.GetDebug() == true {
debug = "TRUE"
}
splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash %s", baseSha))
cmd := c.OSCommand.command(splitCmd[0], splitCmd[1:]...)
gitSequenceEditor := ex
if todo == "" {
gitSequenceEditor = "true"
}
cmd.Env = os.Environ()
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
"LAZYGIT_REBASE_TODO="+todo,
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
)
if overrideEditor {
cmd.Env = append(cmd.Env, "EDITOR="+ex)
}
return cmd, nil
}
func (c *GitCommand) HardReset(baseSha string) error {
return c.OSCommand.RunCommand("git reset --hard " + baseSha)
}
func (c *GitCommand) SoftReset(baseSha string) error {
return c.OSCommand.RunCommand("git reset --soft " + baseSha)
}
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, index int, action string) (string, error) {
if len(commits) <= index+1 {
// assuming they aren't picking the bottom commit
return "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit"))
}
todo := ""
for i, commit := range commits[0 : index+1] {
a := "pick"
if i == index {
a = action
}
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
return todo, nil
}
// AmendTo amends the given commit with whatever files are staged
func (c *GitCommand) AmendTo(sha string) error {
if err := c.OSCommand.RunCommand(fmt.Sprintf("git commit --fixup=%s", sha)); err != nil {
return err
}
return c.RunSkipEditorCommand(
fmt.Sprintf(
"git rebase --interactive --autostash --autosquash %s^", sha,
),
)
}
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
func (c *GitCommand) EditRebaseTodo(index int, action string) error {
fileName := ".git/rebase-merge/git-rebase-todo"
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
// we have the most recent commit at the bottom whereas the todo file has
// it at the bottom, so we need to subtract our index from the commit count
contentIndex := commitCount - 1 - index
splitLine := strings.Split(content[contentIndex], " ")
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
result := strings.Join(content, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
}
func (c *GitCommand) getTodoCommitCount(content []string) int {
// count lines that are not blank and are not comments
commitCount := 0
for _, line := range content {
if line != "" && !strings.HasPrefix(line, "#") {
commitCount++
}
}
return commitCount
}
// MoveTodoDown moves a rebase todo item down by one position
func (c *GitCommand) MoveTodoDown(index int) error {
fileName := ".git/rebase-merge/git-rebase-todo"
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
contentIndex := commitCount - 1 - index
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
result := strings.Join(rearrangedContent, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
}
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha))
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
todo := ""
for _, commit := range commits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}

View File

@@ -9,7 +9,7 @@ import (
"time"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
gogit "gopkg.in/src-d/go-git.v4"
)
@@ -53,23 +53,6 @@ func (f fileInfoMock) Sys() interface{} {
return f.sys
}
func newDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
func newDummyGitCommand() *GitCommand {
return &GitCommand{
Log: newDummyLog(),
OSCommand: newDummyOSCommand(),
Tr: i18n.NewLocalizer(newDummyLog()),
getGlobalGitConfig: func(string) (string, error) { return "", nil },
getLocalGitConfig: func(string) (string, error) { return "", nil },
removeFile: func(string) error { return nil },
}
}
// TestVerifyInGitRepo is a function.
func TestVerifyInGitRepo(t *testing.T) {
type scenario struct {
@@ -276,7 +259,7 @@ func TestNewGitCommand(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.setup()
s.test(NewGitCommand(newDummyLog(), newDummyOSCommand(), i18n.NewLocalizer(newDummyLog())))
s.test(NewGitCommand(NewDummyLog(), NewDummyOSCommand(), i18n.NewLocalizer(NewDummyLog()), NewDummyAppConfig()))
})
}
}
@@ -326,7 +309,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetStashEntries())
@@ -336,7 +319,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
// TestGitCommandGetStashEntryDiff is a function.
func TestGitCommandGetStashEntryDiff(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "show", "-p", "--color", "stash@{1}"}, args)
@@ -428,7 +411,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetStatusFiles())
@@ -438,7 +421,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
// TestGitCommandStashDo is a function.
func TestGitCommandStashDo(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args)
@@ -451,7 +434,7 @@ func TestGitCommandStashDo(t *testing.T) {
// TestGitCommandStashSave is a function.
func TestGitCommandStashSave(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args)
@@ -464,7 +447,7 @@ func TestGitCommandStashSave(t *testing.T) {
// TestGitCommandCommitAmend is a function.
func TestGitCommandCommitAmend(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "--allow-empty"}, args)
@@ -550,7 +533,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles))
})
@@ -608,55 +591,16 @@ func TestGitCommandGetCommitDifferences(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}"))
})
}
}
// TestGitCommandGetCommitsToPush is a function.
func TestGitCommandGetCommitsToPush(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(map[string]bool)
}
scenarios := []scenario{
{
"Can't retrieve pushable commits",
func(string, ...string) *exec.Cmd {
return exec.Command("test")
},
func(pushables map[string]bool) {
assert.EqualValues(t, map[string]bool{}, pushables)
},
},
{
"Retrieve pushable commits",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "8a2bb0e\n78976bc")
},
func(pushables map[string]bool) {
assert.Len(t, pushables, 2)
assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetCommitsToPush())
})
}
}
// TestGitCommandRenameCommit is a function.
func TestGitCommandRenameCommit(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "-m", "test"}, args)
@@ -669,7 +613,7 @@ func TestGitCommandRenameCommit(t *testing.T) {
// TestGitCommandResetToCommit is a function.
func TestGitCommandResetToCommit(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "78976bc"}, args)
@@ -682,7 +626,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
// TestGitCommandNewBranch is a function.
func TestGitCommandNewBranch(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "-b", "test"}, args)
@@ -736,7 +680,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.DeleteBranch(s.branch, s.force))
})
@@ -745,7 +689,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
// TestGitCommandMerge is a function.
func TestGitCommandMerge(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args)
@@ -842,7 +786,7 @@ func TestGitCommandUsingGpg(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.getLocalGitConfig = s.getLocalGitConfig
s.test(gitCmd.usingGpg())
@@ -912,16 +856,16 @@ func TestGitCommandCommit(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Commit("test", false))
s.test(gitCmd.Commit("test"))
})
}
}
// TestGitCommandCommitAmendFromFiles is a function.
func TestGitCommandCommitAmendFromFiles(t *testing.T) {
// TestGitCommandAmendHead is a function.
func TestGitCommandAmendHead(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
@@ -934,7 +878,7 @@ func TestGitCommandCommitAmendFromFiles(t *testing.T) {
"Amend commit using gpg",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "bash", cmd)
assert.EqualValues(t, []string{"-c", `git commit --amend -m 'test'`}, args)
assert.EqualValues(t, []string{"-c", "git commit --amend --no-edit"}, args)
return exec.Command("echo")
},
@@ -950,7 +894,7 @@ func TestGitCommandCommitAmendFromFiles(t *testing.T) {
"Amend commit without using gpg",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "-m", "test"}, args)
assert.EqualValues(t, []string{"commit", "--amend", "--no-edit"}, args)
return exec.Command("echo")
},
@@ -966,7 +910,7 @@ func TestGitCommandCommitAmendFromFiles(t *testing.T) {
"Amend commit without using gpg with an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "-m", "test"}, args)
assert.EqualValues(t, []string{"commit", "--amend", "--no-edit"}, args)
return exec.Command("test")
},
@@ -982,10 +926,10 @@ func TestGitCommandCommitAmendFromFiles(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Commit("test", true))
s.test(gitCmd.AmendHead())
})
}
}
@@ -1010,7 +954,7 @@ func TestGitCommandPush(t *testing.T) {
},
false,
func(err error) {
assert.Contains(t, err.Error(), "error: failed to push some refs")
assert.NoError(t, err)
},
},
{
@@ -1023,7 +967,7 @@ func TestGitCommandPush(t *testing.T) {
},
true,
func(err error) {
assert.Contains(t, err.Error(), "error: failed to push some refs")
assert.NoError(t, err)
},
},
{
@@ -1035,14 +979,14 @@ func TestGitCommandPush(t *testing.T) {
},
false,
func(err error) {
assert.Contains(t, err.Error(), "error: failed to push some refs")
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
err := gitCmd.Push("test", s.forcePush, func(passOrUname string) string {
return "\n"
@@ -1052,137 +996,9 @@ func TestGitCommandPush(t *testing.T) {
}
}
// TestGitCommandSquashPreviousTwoCommits is a function.
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("test")
},
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("test")
},
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"))
})
}
}
// TestGitCommandSquashFixupCommit is a function.
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("test")
}
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"))
})
}
}
// TestGitCommandCatFile is a function.
func TestGitCommandCatFile(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "cat", cmd)
assert.EqualValues(t, []string{"test.txt"}, args)
@@ -1197,7 +1013,7 @@ func TestGitCommandCatFile(t *testing.T) {
// TestGitCommandStageFile is a function.
func TestGitCommandStageFile(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"add", "test.txt"}, args)
@@ -1248,7 +1064,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.UnStageFile("test.txt", s.tracked))
})
@@ -1317,7 +1133,7 @@ func TestGitCommandIsInMergeState(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.IsInMergeState())
})
@@ -1518,7 +1334,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
var cmdsCalled *[][]string
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command, cmdsCalled = s.command()
gitCmd.removeFile = s.removeFile
s.test(cmdsCalled, gitCmd.RemoveFile(s.file))
@@ -1528,16 +1344,62 @@ func TestGitCommandRemoveFile(t *testing.T) {
// TestGitCommandShow is a function.
func TestGitCommandShow(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"show", "--color", "456abcde"}, args)
return exec.Command("echo")
type scenario struct {
testName string
arg string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
_, err := gitCmd.Show("456abcde")
assert.NoError(t, err)
scenarios := []scenario{
{
"regular commit",
"456abcde",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git show --color 456abcde",
Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nblah\"",
},
{
Expect: "git rev-list -1 --merges 456abcde^...456abcde",
Replace: "echo",
},
}),
func(result string, err error) {
assert.NoError(t, err)
assert.Equal(t, "commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nblah\n", result)
},
},
{
"merge commit",
"456abcde",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git show --color 456abcde",
Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nMerge: 1a6a69a 3b51d7c\"",
},
{
Expect: "git rev-list -1 --merges 456abcde^...456abcde",
Replace: "echo aa30e006433628ba9281652952b34d8aacda9c01",
},
{
Expect: "git diff --color 1a6a69a...3b51d7c",
Replace: "echo blah",
},
}),
func(result string, err error) {
assert.NoError(t, err)
assert.Equal(t, "commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nMerge: 1a6a69a 3b51d7c\nblah\n", result)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Show(s.arg))
}
}
// TestGitCommandCheckout is a function.
@@ -1580,7 +1442,7 @@ func TestGitCommandCheckout(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Checkout("test", s.force))
})
@@ -1589,7 +1451,7 @@ func TestGitCommandCheckout(t *testing.T) {
// TestGitCommandGetBranchGraph is a function.
func TestGitCommandGetBranchGraph(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--color", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "-100", "test"}, args)
@@ -1601,171 +1463,6 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
assert.NoError(t, err)
}
// TestGitCommandGetCommits is a function.
func TestGitCommandGetCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*Commit, error)
}
scenarios := []scenario{
{
"No data found",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 0)
},
},
{
"GetCommits returns 2 commits, 1 pushed the other not",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 2)
assert.EqualValues(t, []*Commit{
{
Sha: "8a2bb0e",
Name: "commit 1",
Pushed: true,
Merged: false,
DisplayString: "8a2bb0e commit 1",
},
{
Sha: "78976bc",
Name: "commit 2",
Pushed: false,
Merged: true,
DisplayString: "78976bc commit 2",
},
}, commits)
},
},
{
"GetCommits bubbles up an error from setCommitMergedStatuses",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
// here's where we are returning the error
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
// here too
return exec.Command("test")
}
return nil
},
func(commits []*Commit, err error) {
assert.Error(t, err)
assert.Len(t, commits, 0)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetCommits())
})
}
}
// TestGitCommandGetLog is a function.
func TestGitCommandGetLog(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string)
}
scenarios := []scenario{
{
"Retrieves logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
},
func(output string) {
assert.EqualValues(t, "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line\n", output)
},
},
{
"An error occurred when retrieving logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("test")
},
func(output string) {
assert.Empty(t, output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetLog())
})
}
}
// TestGitCommandDiff is a function.
func TestGitCommandDiff(t *testing.T) {
type scenario struct {
@@ -1841,103 +1538,13 @@ func TestGitCommandDiff(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
gitCmd.Diff(s.file, s.plain)
})
}
}
// TestGitCommandGetMergeBase is a function.
func TestGitCommandGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.getMergeBase())
})
}
}
// TestGitCommandCurrentBranchName is a function.
func TestGitCommandCurrentBranchName(t *testing.T) {
type scenario struct {
@@ -1994,7 +1601,7 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.CurrentBranchName())
})
@@ -2052,9 +1659,55 @@ func TestGitCommandApplyPatch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.ApplyPatch("test"))
})
}
}
// TestGitCommandRebaseBranch is a function.
func TestGitCommandRebaseBranch(t *testing.T) {
type scenario struct {
testName string
arg string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"successful rebase",
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash master",
Replace: "echo",
},
}),
func(err error) {
assert.NoError(t, err)
},
},
{
"unsuccessful rebase",
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash master",
Replace: "test",
},
}),
func(err error) {
assert.Error(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
gitCmd.OSCommand.command = s.command
s.test(gitCmd.RebaseBranch(s.arg))
}
}

View File

@@ -1,13 +1,15 @@
package commands
import (
"errors"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
@@ -48,14 +50,35 @@ func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
}
}
// SetCommand sets the command function used by the struct.
// To be used for testing only
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.command = cmd
}
// RunCommandWithOutput wrapper around commands returning their output and error
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
splitCmd := str.ToArgv(command)
cmd := c.ExecutableFromString(command)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutable runs an executable file and returns an error if there was one
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
_, err := c.RunExecutableWithOutput(cmd)
return err
}
// ExecutableFromString takes a string like `git status` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
c.Log.Info(splitCmd)
return sanitisedCommandOutput(
c.command(splitCmd[0], splitCmd[1:]...).CombinedOutput(),
)
return c.command(splitCmd[0], splitCmd[1:]...)
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
@@ -122,7 +145,7 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", err
return "", errors.Wrap(err, 0)
}
return outputString, errors.New(outputString)
}
@@ -201,12 +224,15 @@ func (c *OSCommand) Unquote(message string) string {
func (c *OSCommand) AppendLineToFile(filename, line string) error {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
return errors.Wrap(err, 0)
}
defer f.Close()
_, err = f.WriteString("\n" + line)
return err
if err != nil {
return errors.Wrap(err, 0)
}
return nil
}
// CreateTempFile writes a string to a new temp file and returns the file's name
@@ -214,16 +240,16 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := ioutil.TempFile("", filename)
if err != nil {
c.Log.Error(err)
return "", err
return "", errors.Wrap(err, 0)
}
if _, err := tmpfile.WriteString(content); err != nil {
c.Log.Error(err)
return "", err
return "", errors.Wrap(err, 0)
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", err
return "", errors.Wrap(err, 0)
}
return tmpfile.Name(), nil
@@ -231,5 +257,42 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
// RemoveFile removes a file at the specified path
func (c *OSCommand) RemoveFile(filename string) error {
return os.Remove(filename)
err := os.Remove(filename)
return errors.Wrap(err, 0)
}
// FileExists checks whether a file exists at the specified path
func (c *OSCommand) FileExists(path string) (bool, error) {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
if err != nil {
if len(outString) == 0 {
return err
}
return errors.New(outString)
}
return nil
}
// GetLazygitPath returns the path of the currently executed file
func (c *OSCommand) GetLazygitPath() string {
ex, err := os.Executable() // get the executable path for git to use
if err != nil {
ex = os.Args[0] // fallback to the first call argument if needed
}
return filepath.ToSlash(ex)
}

View File

@@ -6,30 +6,9 @@ import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
yaml "gopkg.in/yaml.v2"
)
func newDummyOSCommand() *OSCommand {
return NewOSCommand(newDummyLog(), newDummyAppConfig())
}
func newDummyAppConfig() *config.AppConfig {
appConfig := &config.AppConfig{
Name: "lazygit",
Version: "unversioned",
Commit: "",
BuildDate: "",
Debug: false,
BuildSource: "",
UserConfig: viper.New(),
}
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
return appConfig
}
// TestOSCommandRunCommandWithOutput is a function.
func TestOSCommandRunCommandWithOutput(t *testing.T) {
type scenario struct {
@@ -54,7 +33,7 @@ func TestOSCommandRunCommandWithOutput(t *testing.T) {
}
for _, s := range scenarios {
s.test(newDummyOSCommand().RunCommandWithOutput(s.command))
s.test(NewDummyOSCommand().RunCommandWithOutput(s.command))
}
}
@@ -75,7 +54,7 @@ func TestOSCommandRunCommand(t *testing.T) {
}
for _, s := range scenarios {
s.test(newDummyOSCommand().RunCommand(s.command))
s.test(NewDummyOSCommand().RunCommand(s.command))
}
}
@@ -122,7 +101,7 @@ func TestOSCommandOpenFile(t *testing.T) {
}
for _, s := range scenarios {
OSCmd := newDummyOSCommand()
OSCmd := NewDummyOSCommand()
OSCmd.command = s.command
OSCmd.Config.GetUserConfig().Set("os.openCommand", "open {{filename}}")
@@ -251,7 +230,7 @@ func TestOSCommandEditFile(t *testing.T) {
}
for _, s := range scenarios {
OSCmd := newDummyOSCommand()
OSCmd := NewDummyOSCommand()
OSCmd.command = s.command
OSCmd.getGlobalGitConfig = s.getGlobalGitConfig
OSCmd.getenv = s.getenv
@@ -262,7 +241,7 @@ func TestOSCommandEditFile(t *testing.T) {
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := newDummyOSCommand()
osCommand := NewDummyOSCommand()
actual := osCommand.Quote("hello `test`")
@@ -273,7 +252,7 @@ func TestOSCommandQuote(t *testing.T) {
// TestOSCommandQuoteSingleQuote tests the quote function with ' quotes explicitly for Linux
func TestOSCommandQuoteSingleQuote(t *testing.T) {
osCommand := newDummyOSCommand()
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
@@ -286,7 +265,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
// TestOSCommandQuoteDoubleQuote tests the quote function with " quotes explicitly for Linux
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
osCommand := newDummyOSCommand()
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
@@ -299,7 +278,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
// TestOSCommandUnquote is a function.
func TestOSCommandUnquote(t *testing.T) {
osCommand := newDummyOSCommand()
osCommand := NewDummyOSCommand()
actual := osCommand.Unquote(`hello "test"`)
@@ -361,7 +340,7 @@ func TestOSCommandFileType(t *testing.T) {
for _, s := range scenarios {
s.setup()
s.test(newDummyOSCommand().FileType(s.path))
s.test(NewDummyOSCommand().FileType(s.path))
_ = os.RemoveAll(s.path)
}
}
@@ -392,7 +371,7 @@ func TestOSCommandCreateTempFile(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(newDummyOSCommand().CreateTempFile(s.filename, s.content))
s.test(NewDummyOSCommand().CreateTempFile(s.filename, s.content))
})
}
}

View File

@@ -1,9 +1,10 @@
package commands
import (
"errors"
"fmt"
"strings"
"github.com/go-errors/errors"
)
// Service is a service that repository is on (Github, Bitbucket, ...)

View File

@@ -144,7 +144,7 @@ func TestCreatePullRequest(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := newDummyGitCommand()
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.command = s.command
gitCommand.OSCommand.Config.GetUserConfig().Set("os.openLinkCommand", "open {{link}}")
dummyPullRequest := NewPullRequest(gitCommand)

View File

@@ -8,6 +8,6 @@ type StashEntry struct {
}
// GetDisplayStrings returns the dispaly string of branch
func (s *StashEntry) GetDisplayStrings() []string {
func (s *StashEntry) GetDisplayStrings(isFocused bool) []string {
return []string{s.DisplayString}
}

View File

@@ -3,6 +3,7 @@ package config
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"github.com/shibukawa/configdir"
@@ -42,18 +43,22 @@ type AppConfigurer interface {
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag *bool) (*AppConfig, error) {
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
userConfig, err := LoadConfig("config", true)
if err != nil {
return nil, err
}
if os.Getenv("DEBUG") == "TRUE" {
debuggingFlag = true
}
appConfig := &AppConfig{
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: *debuggingFlag,
Debug: debuggingFlag,
BuildSource: buildSource,
UserConfig: userConfig,
AppState: &AppState{},
@@ -229,6 +234,7 @@ func GetDefaultConfig() []byte {
## stuff relating to the UI
scrollHeight: 2
scrollPastBottom: true
mouseEvents: false # will default to true when the feature is complete
theme:
activeBorderColor:
- white
@@ -239,6 +245,9 @@ func GetDefaultConfig() []byte {
- blue
commitLength:
show: true
git:
merging:
manualCommit: false
update:
method: prompt # can be: prompt | background | never
days: 14 # how often a update is checked for

View File

@@ -20,6 +20,9 @@ import (
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry

View File

@@ -0,0 +1,288 @@
package git
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *commands.OSCommand
Tr *i18n.Localizer
CherryPickedCommits []*commands.Commit
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand, tr *i18n.Localizer, cherryPickedCommits []*commands.Commit) (*CommitListBuilder, error) {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
CherryPickedCommits: cherryPickedCommits,
}, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
commits := []*commands.Commit{}
var rebasingCommits []*commands.Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode != "" {
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
}
}
unpushedCommits := c.getUnpushedCommits()
log := c.getLog()
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
_, unpushed := unpushedCommits[sha]
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, &commands.Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Status: status,
DisplayString: strings.Join(splitLine, " "),
})
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere"))
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(commits)
if err != nil {
return nil, err
}
commits, err = c.setCommitCherryPickStatuses(commits)
if err != nil {
return nil, err
}
return commits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) {
switch rebaseMode {
case "normal":
return c.getNormalRebasingCommits()
case "interactive":
return c.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(".git/rebase-apply/rewritten")
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*commands.Commit{}
err = filepath.Walk(".git/rebase-apply", func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
if err != nil {
return err
}
commits = append([]*commands.Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) {
bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo")
if err != nil {
c.Log.Info(fmt.Sprintf("error occured reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
commits := []*commands.Commit{}
lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines {
if line == "" || line == "noop" {
return commits, nil
}
splitLine := strings.Split(line, " ")
commits = append([]*commands.Commit{{
Sha: splitLine[1][0:7],
Name: strings.Join(splitLine[2:], " "),
Status: "rebasing",
Action: splitLine[0],
}}, commits...)
}
return nil, nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*commands.Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1][0:7]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &commands.Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
ancestor, err := c.getMergeBase()
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
for _, commit := range commits {
for _, cherryPickedCommit := range c.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
commit.Copied = true
}
}
}
return commits, nil
}
func (c *CommitListBuilder) getMergeBase() (string, error) {
currentBranch, err := c.GitCommand.CurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
if err != nil {
// swallowing error because it's not a big deal; probably because there are no commits yet
}
return output, nil
}
// getUnpushedCommits Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch, a map is returned to ease look up
func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
if err != nil {
return pushables
}
for _, p := range utils.SplitLines(o) {
pushables[p] = true
}
return pushables
}
// getLog gets the git log (currently limited to 30 commits for performance
// until we work out lazy loading
func (c *CommitListBuilder) getLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}

View File

@@ -0,0 +1,315 @@
package git
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := commands.NewDummyOSCommand()
return &CommitListBuilder{
Log: commands.NewDummyLog(),
GitCommand: commands.NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(commands.NewDummyLog()),
CherryPickedCommits: []*commands.Commit{},
}
}
// TestCommitListBuilderGetUnpushedCommits is a function.
func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(map[string]bool)
}
scenarios := []scenario{
{
"Can't retrieve pushable commits",
func(string, ...string) *exec.Cmd {
return exec.Command("test")
},
func(pushables map[string]bool) {
assert.EqualValues(t, map[string]bool{}, pushables)
},
},
{
"Retrieve pushable commits",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "8a2bb0e\n78976bc")
},
func(pushables map[string]bool) {
assert.Len(t, pushables, 2)
assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getUnpushedCommits())
})
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase())
})
}
}
// TestCommitListBuilderGetLog is a function.
func TestCommitListBuilderGetLog(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string)
}
scenarios := []scenario{
{
"Retrieves logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
},
func(output string) {
assert.EqualValues(t, "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line\n", output)
},
},
{
"An error occurred when retrieving logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("test")
},
func(output string) {
assert.Empty(t, output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getLog())
})
}
}
// TestCommitListBuilderGetCommits is a function.
func TestCommitListBuilderGetCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*commands.Commit, error)
}
scenarios := []scenario{
{
"No data found",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*commands.Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 0)
},
},
{
"GetCommits returns 2 commits, 1 unpushed, the other merged",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*commands.Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 2)
assert.EqualValues(t, []*commands.Commit{
{
Sha: "8a2bb0e",
Name: "commit 1",
Status: "unpushed",
DisplayString: "8a2bb0e commit 1",
},
{
Sha: "78976bc",
Name: "commit 2",
Status: "merged",
DisplayString: "78976bc commit 2",
},
}, commits)
},
},
{
"GetCommits bubbles up an error from setCommitMergedStatuses",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
// here's where we are returning the error
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
// here too
return exec.Command("test")
}
return nil
},
func(commits []*commands.Commit, err error) {
assert.Error(t, err)
assert.Len(t, commits, 0)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.GetCommits())
})
}
}

View File

@@ -1,11 +1,12 @@
package git
import (
"errors"
"regexp"
"strconv"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"

View File

@@ -4,21 +4,17 @@ import (
"io/ioutil"
"testing"
"github.com/sirupsen/logrus"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/stretchr/testify/assert"
)
func newDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
func newDummyPatchModifier() *PatchModifier {
// NewDummyPatchModifier constructs a new dummy patch modifier for testing
func NewDummyPatchModifier() *PatchModifier {
return &PatchModifier{
Log: newDummyLog(),
Log: commands.NewDummyLog(),
}
}
func TestModifyPatchForLine(t *testing.T) {
type scenario struct {
testName string
@@ -68,7 +64,7 @@ func TestModifyPatchForLine(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
p := newDummyPatchModifier()
p := NewDummyPatchModifier()
beforePatch, err := ioutil.ReadFile(s.patchFilename)
if err != nil {
panic("Cannot open file at " + s.patchFilename)

View File

@@ -4,14 +4,17 @@ import (
"io/ioutil"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/stretchr/testify/assert"
)
func newDummyPatchParser() *PatchParser {
// NewDummyPatchParser constructs a new dummy patch parser for testing
func NewDummyPatchParser() *PatchParser {
return &PatchParser{
Log: newDummyLog(),
Log: commands.NewDummyLog(),
}
}
func TestParsePatch(t *testing.T) {
type scenario struct {
testName string
@@ -47,7 +50,7 @@ func TestParsePatch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
p := newDummyPatchParser()
p := NewDummyPatchParser()
beforePatch, err := ioutil.ReadFile(s.patchFilename)
if err != nil {
panic("Cannot open file at " + s.patchFilename)

View File

@@ -1,6 +1,9 @@
package gui
import "github.com/jesseduffield/lazygit/pkg/utils"
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type appStatus struct {
name string
@@ -42,3 +45,26 @@ func (m *statusManager) getStatusString() string {
}
return topStatus.name
}
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
go func() {
gui.g.Update(func(g *gocui.Gui) error {
gui.statusManager.addWaitingStatus(name)
return nil
})
defer gui.g.Update(func(g *gocui.Gui) error {
gui.statusManager.removeStatus(name)
return nil
})
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
return gui.createErrorPanel(gui.g, err.Error())
})
}
}()
return nil
}

View File

@@ -22,6 +22,13 @@ func (gui *Gui) getSelectedBranch() *commands.Branch {
// may want to standardise how these select methods work
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
@@ -53,7 +60,7 @@ func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error {
branch := gui.getSelectedBranch()
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
return gui.renderListPanel(gui.getBranchesView(gui.g), gui.State.Branches)
return gui.renderListPanel(gui.getBranchesView(), gui.State.Branches)
}
// gui.refreshStatus is called at the end of this because that's when we can
@@ -67,9 +74,6 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error {
gui.State.Branches = builder.Build()
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
if err := gui.resetOrigin(gui.getBranchesView(gui.g)); err != nil {
return err
}
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
return err
}
@@ -80,15 +84,30 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error {
}
func (gui *Gui) handleBranchesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleBranchSelect(gui.g, v)
}
func (gui *Gui) handleBranchesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleBranchSelect(gui.g, v)
}
@@ -125,7 +144,7 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
}
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
if err := gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("FetchWait")); err != nil {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("FetchWait")); err != nil {
return err
}
go func() {
@@ -222,16 +241,45 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0]
selectedBranch := gui.getSelectedBranch()
defer gui.refreshSidePanels(g)
if checkedOutBranch.Name == selectedBranch.Name {
checkedOutBranch := gui.State.Branches[0].Name
selectedBranch := gui.getSelectedBranch().Name
if checkedOutBranch == selectedBranch {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
}
if err := gui.GitCommand.Merge(selectedBranch.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
prompt := gui.Tr.TemplateLocalize(
"ConfirmMerge",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranch,
},
)
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("MergingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.Merge(selectedBranch)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
func (gui *Gui) handleRebase(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0].Name
selectedBranch := gui.getSelectedBranch().Name
if selectedBranch == checkedOutBranch {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
}
return nil
prompt := gui.Tr.TemplateLocalize(
"ConfirmRebase",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranch,
},
)
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("RebasingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.RebaseBranch(selectedBranch)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
@@ -243,10 +291,10 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
return nil
}
if branch.Pushables == "?" {
return gui.createErrorPanel(gui.g, "Cannot fast-forward a branch with no upstream")
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdNoUpstream"))
}
if branch.Pushables != "0" {
return gui.createErrorPanel(gui.g, "Cannot fast-forward a branch with commits to push")
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdCommitsToPush"))
}
upstream := "origin" // hardcoding for now
message := gui.Tr.TemplateLocalize(
@@ -257,7 +305,7 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
},
)
go func() {
_ = gui.createMessagePanel(gui.g, v, "", message)
_ = gui.createLoaderPanel(gui.g, v, message)
if err := gui.GitCommand.FastForward(branch.Name); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
} else {

View File

@@ -1,39 +1,49 @@
package gui
import (
"os/exec"
"strconv"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
}
sub, err := gui.GitCommand.Commit(message, false)
// runSyncOrAsyncCommand takes the output of a command that may have returned
// either no error, an error, or a subprocess to execute, and if a subprocess
// needs to be set on the gui object, it does so, and then returns the error
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) error {
if err != nil {
// TODO need to find a way to send through this error
if err != gui.Errors.ErrSubProcess {
return gui.createErrorPanel(g, err.Error())
return gui.createErrorPanel(gui.g, err.Error())
}
}
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
return nil
}
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
}
if err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message)); err != nil {
return err
}
v.Clear()
_ = v.SetCursor(0, 0)
_ = v.SetOrigin(0, 0)
_, _ = g.SetViewOnBottom("commitMessage")
_ = gui.switchFocus(g, v, gui.getFilesView(g))
_ = gui.switchFocus(g, v, gui.getFilesView())
return gui.refreshSidePanels(g)
}
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
g.SetViewOnBottom("commitMessage")
return gui.switchFocus(g, v, gui.getFilesView(g))
return gui.switchFocus(g, v, gui.getFilesView())
}
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
@@ -87,6 +97,6 @@ func (gui *Gui) RenderCommitLength() {
if !gui.Config.GetUserConfig().GetBool("gui.commitLength.show") {
return
}
v := gui.getCommitMessageView(gui.g)
v := gui.getCommitMessageView()
v.Subtitle = gui.getBufferLength(v)
}

View File

@@ -1,11 +1,14 @@
package gui
import (
"errors"
"fmt"
"strconv"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -21,6 +24,13 @@ func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
}
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
@@ -38,7 +48,11 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
commits, err := gui.GitCommand.GetCommits()
builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits)
if err != nil {
return err
}
commits, err := builder.GetCommits()
if err != nil {
return err
}
@@ -46,12 +60,13 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
list, err := utils.RenderList(gui.State.Commits)
isFocused := gui.g.CurrentView().Name() == "commits"
list, err := utils.RenderList(gui.State.Commits, isFocused)
if err != nil {
return err
}
v := gui.getCommitsView(gui.g)
v := gui.getCommitsView()
v.Clear()
fmt.Fprint(v, list)
@@ -65,16 +80,30 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
}
func (gui *Gui) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, v)
}
func (gui *Gui) handleCommitsPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, v)
}
@@ -92,7 +121,7 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
if err := gui.refreshFiles(g); err != nil {
if err := gui.refreshFiles(); err != nil {
panic(err)
}
gui.resetOrigin(commitView)
@@ -102,24 +131,25 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error
}
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit"))
}
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
commit := gui.getSelectedCommit(g)
if commit == nil {
return errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
applied, err := gui.handleMidRebaseCommand("squash")
if err != nil {
return err
}
if err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
if applied {
return nil
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
gui.refreshStatus(g)
return gui.handleCommitSelect(g, v)
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "squash")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return nil
}
// TODO: move to files panel
@@ -136,28 +166,33 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
if gui.anyUnStagedChanges(gui.State.Files) {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantFixupWhileUnstagedChanges"))
applied, err := gui.handleMidRebaseCommand("fixup")
if err != nil {
return err
}
branch := gui.State.Branches[0]
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitsThisBranch"))
if applied {
return nil
}
message := gui.Tr.SLocalize("SureFixupThisCommit")
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.refreshStatus(g)
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return nil
}
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
}
@@ -173,14 +208,232 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
gui.SubProcess = gui.GitCommand.PrepareCommitAmendSubProcess()
g.Update(func(g *gocui.Gui) error {
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if subProcess != nil {
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
})
}
return nil
}
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
if selectedCommit.Status != "rebasing" {
return false, nil
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == "reword" {
return true, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("rewordNotSupported"))
}
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil {
return false, gui.createErrorPanel(gui.g, err.Error())
}
return true, gui.refreshCommits(gui.g)
}
// handleMoveTodoDown like handleMidRebaseCommand but for moving an item up in the todo list
func (gui *Gui) handleMoveTodoDown(index int) (bool, error) {
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status != "rebasing" {
return false, nil
}
if gui.State.Commits[index+1].Status != "rebasing" {
return true, nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return true, gui.createErrorPanel(gui.g, err.Error())
}
return true, gui.refreshCommits(gui.g)
}
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("drop")
if err != nil {
return err
}
if applied {
return nil
}
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("DeleteCommitTitle"), gui.Tr.SLocalize("DeleteCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "drop")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
index := gui.State.Panels.Commits.SelectedLine
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if gui.State.Commits[index+1].Status != "rebasing" {
return nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index)
if err == nil {
gui.State.Panels.Commits.SelectedLine++
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
index := gui.State.Panels.Commits.SelectedLine
if index == 0 {
return nil
}
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine--
return gui.refreshCommits(gui.g)
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1)
if err == nil {
gui.State.Panels.Commits.SelectedLine--
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("edit")
if err != nil {
return err
}
if applied {
return nil
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit")
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("pick")
if err != nil {
return err
}
if applied {
return nil
}
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return gui.pullFiles(g, v)
}
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
}
func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
// get currently selected commit, add the sha to state.
commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
// we will un-copy it if it's already copied
for index, cherryPickedCommit := range gui.State.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
gui.State.CherryPickedCommits = append(gui.State.CherryPickedCommits[0:index], gui.State.CherryPickedCommits[index+1:]...)
return gui.refreshCommits(gui.g)
}
}
gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine)
return gui.refreshCommits(gui.g)
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
// not super happy with modifying the state of the Commits array here
// but the alternative would be very tricky
gui.State.Commits[index].Copied = true
newCommits := []*commands.Commit{}
for _, commit := range gui.State.Commits {
if commit.Copied {
// duplicating just the things we need to put in the rebase TODO list
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
}
}
gui.State.CherryPickedCommits = newCommits
}
func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
// whenever I add a commit, I need to make sure I retain its order
// find the last commit that is copied that's above our position
// if there are none, startIndex = 0
startIndex := 0
for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] {
if commit.Copied {
startIndex = index
}
}
gui.Log.Info("commit copy start index: " + strconv.Itoa(startIndex))
for index := startIndex; index <= gui.State.Panels.Commits.SelectedLine; index++ {
gui.addCommitToCherryPickedCommits(index)
}
return gui.refreshCommits(gui.g)
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}

View File

@@ -63,7 +63,7 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "")
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "", false)
if err != nil {
return err
}
@@ -71,31 +71,43 @@ func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title s
return gui.setKeyBindings(g, handleConfirm, nil)
}
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string) (*gocui.View, error) {
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return nil, err
}
confirmationView.HasLoader = hasLoader
confirmationView.Title = title
confirmationView.Wrap = true
confirmationView.FgColor = gocui.ColorWhite
}
gui.g.Update(func(g *gocui.Gui) error {
confirmationView.Clear()
return gui.switchFocus(gui.g, currentView, confirmationView)
})
return confirmationView, nil
}
func (gui *Gui) onNewPopupPanel() {
_, _ = gui.g.SetViewOnBottom("commitMessage")
_, _ = gui.g.SetViewOnBottom("credentials")
viewNames := []string{"commitMessage",
"credentials",
"menu"}
for _, viewName := range viewNames {
_, _ = gui.g.SetViewOnBottom(viewName)
}
}
func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(g, currentView, "", prompt, true, nil, nil)
}
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose)
}
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
@@ -110,7 +122,7 @@ func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, t
gui.Log.Error(errMessage)
}
}
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt)
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader)
if err != nil {
return err
}
@@ -141,7 +153,7 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
}
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return gui.createConfirmationPanel(g, currentView, title, prompt, nil, nil)
return gui.createPopupPanel(g, currentView, title, prompt, false, nil, nil)
}
// createSpecificErrorPanel allows you to create an error popup, specifying the

75
pkg/gui/context.go Normal file
View File

@@ -0,0 +1,75 @@
package gui
func (gui *Gui) titleMap() map[string]string {
return map[string]string{
"commits": gui.Tr.SLocalize("DiffTitle"),
"branches": gui.Tr.SLocalize("LogTitle"),
"files": gui.Tr.SLocalize("DiffTitle"),
"status": "",
"stash": gui.Tr.SLocalize("DiffTitle"),
}
}
func (gui *Gui) contextTitleMap() map[string]map[string]string {
return map[string]map[string]string{
"main": {
"staging": gui.Tr.SLocalize("StagingMainTitle"),
"merging": gui.Tr.SLocalize("MergingMainTitle"),
"normal": "",
},
}
}
func (gui *Gui) setMainTitle() error {
currentViewName := gui.g.CurrentView().Name()
var newTitle string
if context, ok := gui.State.Contexts[currentViewName]; ok {
newTitle = gui.contextTitleMap()[currentViewName][context]
} else if title, ok := gui.titleMap()[currentViewName]; ok {
newTitle = title
} else {
return nil
}
gui.getMainView().Title = newTitle
return nil
}
func (gui *Gui) changeContext(viewName, context string) error {
if gui.State.Contexts[viewName] == context {
return nil
}
contextMap := gui.GetContextMap()
gui.g.DeleteKeybindings(viewName)
bindings := contextMap[viewName][context]
for _, binding := range bindings {
if err := gui.g.SetKeybinding(viewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
gui.State.Contexts[viewName] = context
return gui.setMainTitle()
}
func (gui *Gui) setInitialContexts() error {
contextMap := gui.GetContextMap()
initialContexts := map[string]string{
"main": "normal",
}
for viewName, context := range initialContexts {
bindings := contextMap[viewName][context]
for _, binding := range bindings {
if err := gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
}
gui.State.Contexts = initialContexts
return nil
}

View File

@@ -36,7 +36,7 @@ func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUn
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
gui.credentials <- message
err := gui.refreshFiles(g)
err := gui.refreshFiles()
if err != nil {
return err
}
@@ -51,7 +51,7 @@ func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
}
nextView, err := gui.g.View("confirmation")
if err != nil {
nextView = gui.getFilesView(g)
nextView = gui.getFilesView()
}
err = gui.switchFocus(g, nil, nextView)
if err != nil {
@@ -67,7 +67,7 @@ func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
}
gui.credentials <- ""
return gui.switchFocus(g, nil, gui.getFilesView(g))
return gui.switchFocus(g, nil, gui.getFilesView())
}
func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error {
@@ -96,7 +96,7 @@ func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr er
errMessage = gui.Tr.SLocalize("PassUnameWrong")
}
// we are not logging this error because it may contain a password
_ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(gui.g), false)
_ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(), false)
} else {
_ = gui.closeConfirmationPrompt(g)
_ = gui.refreshSidePanels(g)

View File

@@ -26,7 +26,35 @@ func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
return gui.State.Files[selectedLine], nil
}
func (gui *Gui) handleFilesFocus(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
cx, cy := v.Cursor()
_, oy := v.Origin()
prevSelectedLine := gui.State.Panels.Files.SelectedLine
newSelectedLine := cy - oy
if newSelectedLine > len(gui.State.Files)-1 || len(utils.Decolorise(gui.State.Files[newSelectedLine].DisplayString)) < cx {
return gui.handleFileSelect(gui.g, v, false)
}
gui.State.Panels.Files.SelectedLine = newSelectedLine
if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() {
return gui.handleFilePress(gui.g, v)
} else {
return gui.handleFileSelect(gui.g, v, true)
}
}
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bool) error {
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
@@ -35,37 +63,37 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bo
return gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
}
if file.HasMergeConflicts {
return gui.refreshMergePanel(g)
}
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, v); err != nil {
return err
}
if file.HasInlineMergeConflicts {
return gui.refreshMergePanel()
}
content := gui.GitCommand.Diff(file, false)
if alreadySelected {
g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getMainView(gui.g), content)
return gui.setViewContent(gui.g, gui.getMainView(), content)
})
return nil
}
return gui.renderString(g, "main", content)
}
func (gui *Gui) refreshFiles(g *gocui.Gui) error {
func (gui *Gui) refreshFiles() error {
selectedFile, _ := gui.getSelectedFile(gui.g)
filesView, err := g.View("files")
if err != nil {
filesView := gui.getFilesView()
if err := gui.refreshStateFiles(); err != nil {
return err
}
gui.refreshStateFiles()
gui.g.Update(func(g *gocui.Gui) error {
filesView.Clear()
list, err := utils.RenderList(gui.State.Files)
isFocused := gui.g.CurrentView().Name() == "files"
list, err := utils.RenderList(gui.State.Files, isFocused)
if err != nil {
return err
}
@@ -83,6 +111,10 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error {
}
func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Files
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), false)
@@ -90,6 +122,10 @@ func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleFilesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Files
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), true)
@@ -128,11 +164,7 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
return gui.GitCommand.StageFile(file.Name)
}
func (gui *Gui) handleSwitchToStagingPanel(g *gocui.Gui, v *gocui.View) error {
stagingView, err := g.View("staging")
if err != nil {
return err
}
func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
@@ -140,11 +172,16 @@ func (gui *Gui) handleSwitchToStagingPanel(g *gocui.Gui, v *gocui.View) error {
}
return nil
}
if !file.HasUnstagedChanges {
gui.Log.WithField("staging", "staging").Info("making error panel")
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
if !file.HasUnstagedChanges || file.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
}
if err := gui.switchFocus(g, v, stagingView); err != nil {
if err := gui.changeContext("main", "staging"); err != nil {
return err
}
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
return gui.refreshStagingPanel()
@@ -159,7 +196,7 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
return err
}
if file.HasMergeConflicts {
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
@@ -169,7 +206,7 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
}
if err := gui.refreshFiles(g); err != nil {
if err := gui.refreshFiles(); err != nil {
return err
}
@@ -196,7 +233,11 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
_ = gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles(g)
if err := gui.refreshFiles(); err != nil {
return err
}
return gui.handleFileSelect(g, v, false)
}
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
@@ -243,7 +284,7 @@ func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RemoveFile(file); err != nil {
return err
}
return gui.refreshFiles(g)
return gui.refreshFiles()
}, nil)
}
@@ -258,14 +299,14 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles(g)
return gui.refreshFiles()
}
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
commitMessageView := gui.getCommitMessageView(g)
commitMessageView := gui.getCommitMessageView()
g.Update(func(g *gocui.Gui) error {
g.SetViewOnTop("commitMessage")
gui.switchFocus(g, filesView, commitMessageView)
@@ -276,21 +317,19 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
}
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
question := gui.Tr.SLocalize("SureToAmend")
if len(gui.State.Commits) == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
}
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
question := gui.Tr.SLocalize("SureToAmend")
return gui.createConfirmationPanel(g, filesView, title, question, func(g *gocui.Gui, v *gocui.View) error {
lastCommitMsg := gui.State.Commits[0].Name
_, err := gui.GitCommand.Commit(lastCommitMsg, true)
if err != nil {
return gui.createErrorPanel(g, err.Error())
if err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead()); err != nil {
return err
}
return gui.refreshSidePanels(g)
@@ -300,7 +339,7 @@ func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) erro
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
gui.PrepareSubProcess(g, "git", "commit")
@@ -316,15 +355,7 @@ func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) {
}
func (gui *Gui) editFile(filename string) error {
sub, err := gui.OSCommand.EditFile(filename)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
return nil
return gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
@@ -345,24 +376,15 @@ func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return gui.refreshFiles(g)
return gui.refreshFiles()
}
func (gui *Gui) refreshStateFiles() {
func (gui *Gui) refreshStateFiles() error {
// get files to stage
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
gui.updateHasMergeConflictStatus()
}
func (gui *Gui) updateHasMergeConflictStatus() error {
merging, err := gui.GitCommand.IsInMergeState()
if err != nil {
return err
}
gui.State.HasMergeConflicts = merging
return nil
return gui.updateWorkTreeState()
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
@@ -385,9 +407,10 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
}
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
if err := gui.createMessagePanel(gui.g, v, "", gui.Tr.SLocalize("PullWait")); err != nil {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil {
return err
}
go func() {
unamePassOpend := false
err := gui.GitCommand.Pull(func(passOrUname string) string {
@@ -400,7 +423,7 @@ func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error {
if err := gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PushWait")); err != nil {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil {
return err
}
go func() {
@@ -428,10 +451,6 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
mergeView, err := g.View("main")
if err != nil {
return err
}
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
@@ -439,11 +458,16 @@ func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
}
return nil
}
if !file.HasMergeConflicts {
if !file.HasInlineMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
}
gui.switchFocus(g, v, mergeView)
return gui.refreshMergePanel(g)
if err := gui.changeContext("main", "merging"); err != nil {
return err
}
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
return gui.refreshMergePanel()
}
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
@@ -452,7 +476,7 @@ func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
}
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted"))
gui.refreshStatus(g)
return gui.refreshFiles(g)
return gui.refreshFiles()
}
func (gui *Gui) handleResetAndClean(g *gocui.Gui, v *gocui.View) error {
@@ -460,7 +484,7 @@ func (gui *Gui) handleResetAndClean(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.ResetAndClean(); err != nil {
gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles(g)
return gui.refreshFiles()
}, nil)
}
@@ -470,3 +494,25 @@ func (gui *Gui) openFile(filename string) error {
}
return nil
}
func (gui *Gui) anyFilesWithMergeConflicts() bool {
for _, file := range gui.State.Files {
if file.HasMergeConflicts {
return true
}
}
return false
}
func (gui *Gui) handleSoftReset(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("SoftReset"), gui.Tr.SLocalize("ConfirmSoftReset"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.SoftReset("HEAD^"); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(gui.g); err != nil {
return err
}
return gui.refreshFiles()
}, nil)
}

View File

@@ -1,19 +1,20 @@
package gui
import (
"math"
"sync"
// "io"
// "io/ioutil"
"errors"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"time"
"github.com/go-errors/errors"
// "strings"
"github.com/fatih/color"
@@ -86,6 +87,13 @@ type stagingPanelState struct {
Diff string
}
type mergingPanelState struct {
ConflictIndex int
ConflictTop bool
Conflicts []commands.Conflict
EditHistory *stack.Stack
}
type filePanelState struct {
SelectedLine int
}
@@ -108,48 +116,50 @@ type menuPanelState struct {
type panelStates struct {
Files *filePanelState
Staging *stagingPanelState
Branches *branchPanelState
Commits *commitPanelState
Stash *stashPanelState
Menu *menuPanelState
Staging *stagingPanelState
Merging *mergingPanelState
}
type guiState struct {
Files []*commands.File
Branches []*commands.Branch
Commits []*commands.Commit
StashEntries []*commands.StashEntry
PreviousView string
HasMergeConflicts bool
ConflictIndex int
ConflictTop bool
Conflicts []commands.Conflict
EditHistory *stack.Stack
Platform commands.Platform
Updating bool
Panels *panelStates
Files []*commands.File
Branches []*commands.Branch
Commits []*commands.Commit
StashEntries []*commands.StashEntry
PreviousView string
Platform commands.Platform
Updating bool
Panels *panelStates
WorkingTreeState string // one of "merging", "rebasing", "normal"
Contexts map[string]string
CherryPickedCommits []*commands.Commit
}
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
initialState := guiState{
Files: make([]*commands.File, 0),
PreviousView: "files",
Commits: make([]*commands.Commit, 0),
StashEntries: make([]*commands.StashEntry, 0),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: make([]commands.Conflict, 0),
EditHistory: stack.New(),
Platform: *oSCommand.Platform,
Files: make([]*commands.File, 0),
PreviousView: "files",
Commits: make([]*commands.Commit, 0),
CherryPickedCommits: make([]*commands.Commit, 0),
StashEntries: make([]*commands.StashEntry, 0),
Platform: *oSCommand.Platform,
Panels: &panelStates{
Files: &filePanelState{SelectedLine: -1},
Branches: &branchPanelState{SelectedLine: 0},
Commits: &commitPanelState{SelectedLine: -1},
Stash: &stashPanelState{SelectedLine: -1},
Menu: &menuPanelState{SelectedLine: 0},
Merging: &mergingPanelState{
ConflictIndex: 0,
ConflictTop: true,
Conflicts: []commands.Conflict{},
EditHistory: stack.New(),
},
},
}
@@ -172,10 +182,8 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy >= 1 {
return mainView.SetOrigin(ox, oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
}
return nil
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))))
return mainView.SetOrigin(ox, newOy)
}
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
@@ -203,18 +211,82 @@ func max(a, b int) int {
return b
}
// getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var focusedView *gocui.View
return func(g *gocui.Gui) error {
v := gui.g.CurrentView()
if v != focusedView {
if err := gui.onFocusChange(); err != nil {
return err
}
if err := gui.onFocusLost(focusedView); err != nil {
return err
}
if err := gui.onFocus(v); err != nil {
return err
}
focusedView = v
}
return nil
}
}
func (gui *Gui) onFocusChange() error {
currentView := gui.g.CurrentView()
for _, view := range gui.g.Views() {
view.Highlight = view == currentView
}
return gui.setMainTitle()
}
func (gui *Gui) onFocusLost(v *gocui.View) error {
if v == nil {
return nil
}
if v.Name() == "branches" {
if err := gui.renderListPanel(gui.getBranchesView(), gui.State.Branches); err != nil {
return err
}
} else if v.Name() == "main" {
// if we have lost focus to a popup panel, that's okay
if gui.popupPanelFocused() {
return nil
}
if err := gui.changeContext("main", "normal"); err != nil {
return err
}
}
gui.Log.Info(v.Name() + " focus lost")
return nil
}
func (gui *Gui) onFocus(v *gocui.View) error {
if v == nil {
return nil
}
gui.Log.Info(v.Name() + " focus gained")
return nil
}
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
width, height := g.Size()
version := gui.Config.GetVersion()
information := gui.Config.GetVersion()
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate"))
information = donate + " " + information
}
leftSideWidth := width / 3
statusFilesBoundary := 2
filesBranchesBoundary := 2 * height / 5 // height - 20
commitsBranchesBoundary := 3 * height / 5 // height - 10
commitsStashBoundary := height - 5 // height - 5
optionsVersionBoundary := width - max(len(version), 1)
minimumHeight := 16
filesBranchesBoundary := 2 * height / 5
commitsBranchesBoundary := 3 * height / 5
optionsTop := height - 2
commitsStashBoundary := optionsTop - 3
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
minimumHeight := 18
minimumWidth := 10
appStatus := gui.statusManager.getStatusString()
@@ -231,7 +303,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, max(width-1, 2), max(height-1, 2), 0)
if err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
@@ -239,21 +311,13 @@ func (gui *Gui) layout(g *gocui.Gui) error {
g.SetViewOnTop("limit")
}
return nil
} else {
_, _ = g.SetViewOnBottom("limit")
}
_, _ = g.SetViewOnBottom("limit")
g.DeleteView("limit")
optionsTop := height - 2
// hiding options if there's not enough space
if height < 30 {
optionsTop = height - 1
}
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
if err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("DiffTitle")
@@ -261,21 +325,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.FgColor = gocui.ColorWhite
}
v, err = g.SetView("staging", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("StagingTitle")
v.Highlight = true
v.FgColor = gocui.ColorWhite
if _, err := g.SetViewOnBottom("staging"); err != nil {
return err
}
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("StatusTitle")
@@ -284,7 +335,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return err
}
filesView.Highlight = true
@@ -294,31 +345,33 @@ func (gui *Gui) layout(g *gocui.Gui) error {
branchesView, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return err
}
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
commitsView, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("CommitsTitle")
v.FgColor = gocui.ColorWhite
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
stashView, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("StashTitle")
v.FgColor = gocui.ColorWhite
stashView.Title = gui.Tr.SLocalize("StashTitle")
stashView.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, optionsTop, optionsVersionBoundary-1, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return err
}
v.Frame = false
@@ -327,10 +380,10 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
if gui.getCommitMessageView(g) == nil {
if gui.getCommitMessageView() == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width/2, height/2, 0); err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return err
}
g.SetViewOnBottom("commitMessage")
@@ -344,7 +397,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if check, _ := g.View("credentials"); check == nil {
// doesn't matter where this view starts because it will be hidden
if credentialsView, err := g.SetView("credentials", 0, 0, width/2, height/2, 0); err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return err
}
_, err := g.SetViewOnBottom("credentials")
@@ -359,7 +412,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
if appStatusView, err := g.SetView("appStatus", -1, optionsTop, width, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
if err.Error() != "unknown view" {
return err
}
appStatusView.BgColor = gocui.ColorDefault
@@ -370,14 +423,14 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
if v, err := g.SetView("version", optionsVersionBoundary-1, optionsTop, width, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
if v, err := g.SetView("information", optionsVersionBoundary-1, optionsTop, width, optionsTop+2, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorGreen
v.Frame = false
if err := gui.renderString(g, "version", version); err != nil {
if err := gui.renderString(g, "information", information); err != nil {
return err
}
@@ -411,6 +464,13 @@ func (gui *Gui) layout(g *gocui.Gui) error {
listViews := map[*gocui.View]int{
filesView: gui.State.Panels.Files.SelectedLine,
branchesView: gui.State.Panels.Branches.SelectedLine,
commitsView: gui.State.Panels.Commits.SelectedLine,
stashView: gui.State.Panels.Stash.SelectedLine,
}
// menu view might not exist so we check to be safe
if menuView, err := gui.g.View("menu"); err == nil {
listViews[menuView] = gui.State.Panels.Menu.SelectedLine
}
for view, selectedLine := range listViews {
// check if the selected line is now out of view and if so refocus it
@@ -456,24 +516,7 @@ func (gui *Gui) fetch(g *gocui.Gui, v *gocui.View, canAskForCredentials bool) (u
return unamePassOpend, err
}
func (gui *Gui) updateLoader(g *gocui.Gui) error {
gui.g.Update(func(g *gocui.Gui) error {
if view, _ := g.View("confirmation"); view != nil {
content := gui.trimmedContent(view)
if strings.Contains(content, "...") {
staticContent := strings.Split(content, "...")[0] + "..."
if err := gui.setViewContent(g, view, staticContent+" "+utils.Loader()); err != nil {
return err
}
}
}
return nil
})
return nil
}
func (gui *Gui) renderAppStatus(g *gocui.Gui) error {
func (gui *Gui) renderAppStatus() error {
appStatus := gui.statusManager.getStatusString()
if appStatus != "" {
return gui.renderString(gui.g, "appStatus", appStatus)
@@ -490,10 +533,10 @@ func (gui *Gui) renderGlobalOptions() error {
})
}
func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
go func() {
for range time.Tick(interval) {
function(g)
_ = function()
}
}()
}
@@ -506,6 +549,10 @@ func (gui *Gui) Run() error {
}
defer g.Close()
if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") {
g.Mouse = true
}
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
if err := gui.SetColorScheme(); err != nil {
@@ -528,17 +575,16 @@ func (gui *Gui) Run() error {
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
_ = gui.createConfirmationPanel(g, g.CurrentView(), gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil)
} else {
gui.goEvery(g, time.Second*60, func(g *gocui.Gui) error {
_, err := gui.fetch(g, g.CurrentView(), false)
gui.goEvery(time.Second*60, func() error {
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
return err
})
}
}()
gui.goEvery(g, time.Second*10, gui.refreshFiles)
gui.goEvery(g, time.Millisecond*50, gui.updateLoader)
gui.goEvery(g, time.Millisecond*50, gui.renderAppStatus)
gui.goEvery(time.Second*10, gui.refreshFiles)
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
g.SetManagerFunc(gui.layout)
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
if err = gui.keybindings(g); err != nil {
return err
@@ -551,7 +597,7 @@ func (gui *Gui) Run() error {
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
// otherwise it handles the error, possibly by quitting the application
func (gui *Gui) RunWithSubprocesses() {
func (gui *Gui) RunWithSubprocesses() error {
for {
if err := gui.Run(); err != nil {
if err == gocui.ErrQuit {
@@ -568,10 +614,11 @@ func (gui *Gui) RunWithSubprocesses() {
gui.SubProcess.Stdin = nil
gui.SubProcess = nil
} else {
log.Panicln(err)
return err
}
}
}
return nil
}
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
@@ -585,3 +632,15 @@ func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
}
return gocui.ErrQuit
}
func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
if !gui.g.Mouse {
return nil
}
cx, _ := v.Cursor()
if cx > len(gui.Tr.SLocalize("Donate")) {
return nil
}
return gui.OSCommand.OpenLink("https://donorbox.org/lazygit")
}

View File

@@ -16,7 +16,7 @@ type Binding struct {
}
// GetDisplayStrings returns the display string of a file
func (b *Binding) GetDisplayStrings() []string {
func (b *Binding) GetDisplayStrings(isFocused bool) []string {
return []string{b.GetKey(), b.Description}
}
@@ -39,13 +39,25 @@ func (b *Binding) GetKey() string {
return "enter"
case 32:
return "space"
case 65514:
return "►"
case 65515:
return "◄"
case 65517:
return "▲"
case 65516:
return "▼"
case 65508:
return "PgUp"
case 65507:
return "PgDn"
}
return string(key)
}
// GetKeybindings is a function.
func (gui *Gui) GetKeybindings() []*Binding {
// GetInitialKeybindings is a function.
func (gui *Gui) GetInitialKeybindings() []*Binding {
bindings := []*Binding{
{
ViewName: "",
@@ -82,6 +94,12 @@ func (gui *Gui) GetKeybindings() []*Binding {
Key: gocui.KeyCtrlD,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
}, {
ViewName: "",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleCreateRebaseOptionsMenu,
Description: gui.Tr.SLocalize("ViewMergeRebaseOptions"),
}, {
ViewName: "",
Key: 'P',
@@ -160,12 +178,6 @@ func (gui *Gui) GetKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleFileRemove,
Description: gui.Tr.SLocalize("removeFile"),
}, {
ViewName: "files",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleSwitchToMerge,
Description: gui.Tr.SLocalize("resolveMergeConflicts"),
}, {
ViewName: "files",
Key: 'e',
@@ -198,10 +210,10 @@ func (gui *Gui) GetKeybindings() []*Binding {
Description: gui.Tr.SLocalize("stashFiles"),
}, {
ViewName: "files",
Key: 'M',
Key: 's',
Modifier: gocui.ModNone,
Handler: gui.handleAbortMerge,
Description: gui.Tr.SLocalize("abortMerge"),
Handler: gui.handleSoftReset,
Description: gui.Tr.SLocalize("softReset"),
}, {
ViewName: "files",
Key: 'a',
@@ -224,7 +236,7 @@ func (gui *Gui) GetKeybindings() []*Binding {
ViewName: "files",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleSwitchToStagingPanel,
Handler: gui.handleEnterFile,
Description: gui.Tr.SLocalize("StageLines"),
}, {
ViewName: "files",
@@ -232,66 +244,6 @@ func (gui *Gui) GetKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleGitFetch,
Description: gui.Tr.SLocalize("fetch"),
}, {
ViewName: "main",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleEscapeMerge,
}, {
ViewName: "main",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handlePickHunk,
}, {
ViewName: "main",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handlePickBothHunks,
}, {
ViewName: "main",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
}, {
ViewName: "main",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextConflict,
}, {
ViewName: "main",
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
}, {
ViewName: "main",
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
}, {
ViewName: "main",
Key: 'h',
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
}, {
ViewName: "main",
Key: 'l',
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextConflict,
}, {
ViewName: "main",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
}, {
ViewName: "main",
Key: 'j',
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
}, {
ViewName: "main",
Key: 'z',
Modifier: gocui.ModNone,
Handler: gui.handlePopFileSnapshot,
}, {
ViewName: "branches",
Key: gocui.KeySpace,
@@ -330,7 +282,13 @@ func (gui *Gui) GetKeybindings() []*Binding {
Description: gui.Tr.SLocalize("deleteBranch"),
}, {
ViewName: "branches",
Key: 'm',
Key: 'r',
Modifier: gocui.ModNone,
Handler: gui.handleRebase,
Description: gui.Tr.SLocalize("rebaseBranch"),
}, {
ViewName: "branches",
Key: 'M',
Modifier: gocui.ModNone,
Handler: gui.handleMerge,
Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"),
@@ -370,6 +328,66 @@ func (gui *Gui) GetKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleCommitFixup,
Description: gui.Tr.SLocalize("fixupCommit"),
}, {
ViewName: "commits",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleCommitDelete,
Description: gui.Tr.SLocalize("deleteCommit"),
}, {
ViewName: "commits",
Key: 'J',
Modifier: gocui.ModNone,
Handler: gui.handleCommitMoveDown,
Description: gui.Tr.SLocalize("moveDownCommit"),
}, {
ViewName: "commits",
Key: 'K',
Modifier: gocui.ModNone,
Handler: gui.handleCommitMoveUp,
Description: gui.Tr.SLocalize("moveUpCommit"),
}, {
ViewName: "commits",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleCommitEdit,
Description: gui.Tr.SLocalize("editCommit"),
}, {
ViewName: "commits",
Key: 'A',
Modifier: gocui.ModNone,
Handler: gui.handleCommitAmendTo,
Description: gui.Tr.SLocalize("amendToCommit"),
}, {
ViewName: "commits",
Key: 'p',
Modifier: gocui.ModNone,
Handler: gui.handleCommitPick,
Description: gui.Tr.SLocalize("pickCommit"),
}, {
ViewName: "commits",
Key: 't',
Modifier: gocui.ModNone,
Handler: gui.handleCommitRevert,
Description: gui.Tr.SLocalize("revertCommit"),
}, {
ViewName: "commits",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleCopyCommit,
Description: gui.Tr.SLocalize("cherryPickCopy"),
}, {
ViewName: "commits",
Key: 'C',
Modifier: gocui.ModNone,
Handler: gui.handleCopyCommitRange,
Description: gui.Tr.SLocalize("cherryPickCopyRange"),
}, {
ViewName: "commits",
Key: 'v',
Modifier: gocui.ModNone,
Handler: gui.HandlePasteCommits,
Description: gui.Tr.SLocalize("pasteCommits"),
}, {
ViewName: "stash",
Key: gocui.KeySpace,
@@ -419,63 +437,10 @@ func (gui *Gui) GetKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
}, {
ViewName: "staging",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleStagingEscape,
Description: gui.Tr.SLocalize("EscapeStaging"),
}, {
ViewName: "staging",
Key: gocui.KeyArrowUp,
ViewName: "information",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "staging",
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "staging",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "staging",
Key: 'j',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "staging",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
}, {
ViewName: "staging",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
}, {
ViewName: "staging",
Key: 'h',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
}, {
ViewName: "staging",
Key: 'l',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
}, {
ViewName: "staging",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleStageLine,
Description: gui.Tr.SLocalize("StageLine"),
}, {
ViewName: "staging",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleStageHunk,
Description: gui.Tr.SLocalize("StageHunk"),
Handler: gui.handleDonate,
},
}
@@ -492,33 +457,231 @@ func (gui *Gui) GetKeybindings() []*Binding {
listPanelMap := map[string]struct {
prevLine func(*gocui.Gui, *gocui.View) error
nextLine func(*gocui.Gui, *gocui.View) error
focus func(*gocui.Gui, *gocui.View) error
}{
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine},
"files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine},
"branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine},
"commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine},
"stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine},
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, focus: gui.handleMenuSelect},
"files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine, focus: gui.handleFilesFocus},
"branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine, focus: gui.handleBranchSelect},
"commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine, focus: gui.handleCommitSelect},
"stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine, focus: gui.handleStashEntrySelect},
"status": {focus: gui.handleStatusSelect},
}
for viewName, functions := range listPanelMap {
bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: functions.prevLine},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: functions.prevLine},
{ViewName: viewName, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: functions.prevLine},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.nextLine},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.nextLine},
{ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: functions.nextLine},
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.focus},
}...)
}
return bindings
}
// GetCurrentKeybindings gets the list of keybindings given the current context
func (gui *Gui) GetCurrentKeybindings() []*Binding {
bindings := gui.GetInitialKeybindings()
viewName := gui.currentViewName()
currentContext := gui.State.Contexts[viewName]
contextBindings := gui.GetContextMap()[viewName][currentContext]
return append(bindings, contextBindings...)
}
func (gui *Gui) keybindings(g *gocui.Gui) error {
bindings := gui.GetKeybindings()
bindings := gui.GetInitialKeybindings()
for _, binding := range bindings {
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
if err := gui.setInitialContexts(); err != nil {
return err
}
return nil
}
func (gui *Gui) GetContextMap() map[string]map[string][]*Binding {
return map[string]map[string][]*Binding{
"main": {
"normal": {
{
ViewName: "main",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
Description: gui.Tr.SLocalize("ScrollDown"),
}, {
ViewName: "main",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
Description: gui.Tr.SLocalize("ScrollUp"),
},
},
"staging": {
{
ViewName: "main",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleStagingEscape,
Description: gui.Tr.SLocalize("EscapeStaging"),
}, {
ViewName: "main",
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
Description: gui.Tr.SLocalize("PrevLine"),
}, {
ViewName: "main",
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
Description: gui.Tr.SLocalize("NextLine"),
}, {
ViewName: "main",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "main",
Key: 'j',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "main",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "main",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "main",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
Description: gui.Tr.SLocalize("PrevHunk"),
}, {
ViewName: "main",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
Description: gui.Tr.SLocalize("NextHunk"),
}, {
ViewName: "main",
Key: 'h',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
}, {
ViewName: "main",
Key: 'l',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
}, {
ViewName: "main",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleStageLine,
Description: gui.Tr.SLocalize("StageLine"),
}, {
ViewName: "main",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleStageHunk,
Description: gui.Tr.SLocalize("StageHunk"),
},
},
"merging": {
{
ViewName: "main",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleEscapeMerge,
Description: gui.Tr.SLocalize("EscapeStaging"),
}, {
ViewName: "main",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handlePickHunk,
Description: gui.Tr.SLocalize("PickHunk"),
}, {
ViewName: "main",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handlePickBothHunks,
Description: gui.Tr.SLocalize("PickBothHunks"),
}, {
ViewName: "main",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
Description: gui.Tr.SLocalize("PrevConflict"),
}, {
ViewName: "main",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextConflict,
Description: gui.Tr.SLocalize("NextConflict"),
}, {
ViewName: "main",
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
Description: gui.Tr.SLocalize("SelectTop"),
}, {
ViewName: "main",
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
Description: gui.Tr.SLocalize("SelectBottom"),
}, {
ViewName: "main",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
}, {
ViewName: "main",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
}, {
ViewName: "main",
Key: 'h',
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
}, {
ViewName: "main",
Key: 'l',
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextConflict,
}, {
ViewName: "main",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
}, {
ViewName: "main",
Key: 'j',
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
}, {
ViewName: "main",
Key: 'z',
Modifier: gocui.ModNone,
Handler: gui.handlePopFileSnapshot,
Description: gui.Tr.SLocalize("Undo"),
},
},
},
}
}

View File

@@ -2,7 +2,6 @@ package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -40,8 +39,10 @@ func (gui *Gui) renderMenuOptions() error {
}
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
if err := g.DeleteKeybinding("menu", gocui.KeySpace, gocui.ModNone); err != nil {
return err
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} {
if err := g.DeleteKeybinding("menu", key, gocui.ModNone); err != nil {
return err
}
}
err := g.DeleteView("menu")
if err != nil {
@@ -50,15 +51,16 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
return gui.returnFocus(g, v)
}
func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error {
list, err := utils.RenderList(items)
func (gui *Gui) createMenu(title string, items interface{}, handlePress func(int) error) error {
isFocused := gui.g.CurrentView().Name() == "menu"
list, err := utils.RenderList(items, isFocused)
if err != nil {
return err
}
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = strings.Title(gui.Tr.SLocalize("menu"))
menuView.Title = title
menuView.FgColor = gocui.ColorWhite
menuView.Clear()
fmt.Fprint(menuView, list)
@@ -66,16 +68,29 @@ func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
selectedLine := gui.State.Panels.Menu.SelectedLine
return handlePress(selectedLine)
if err := handlePress(selectedLine); err != nil {
return err
}
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
}
}
return gui.returnFocus(gui.g, menuView)
}
if err := gui.g.SetKeybinding("menu", gocui.KeySpace, gocui.ModNone, wrappedHandlePress); err != nil {
return err
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} {
if err := gui.g.SetKeybinding("menu", key, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
}
gui.g.Update(func(g *gocui.Gui) error {
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
if _, err := gui.g.View("menu"); err == nil {
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
}
}
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView)

View File

@@ -11,6 +11,7 @@ import (
"strings"
"github.com/fatih/color"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -20,11 +21,13 @@ func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
conflicts := make([]commands.Conflict, 0)
var newConflict commands.Conflict
for i, line := range utils.SplitLines(content) {
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
trimmedLine := strings.TrimPrefix(line, "++")
gui.Log.Info(trimmedLine)
if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" {
newConflict = commands.Conflict{Start: i}
} else if line == "=======" {
} else if trimmedLine == "=======" {
newConflict.Middle = i
} else if strings.HasPrefix(line, ">>>>>>> ") {
} else if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
newConflict.End = i
conflicts = append(conflicts, newConflict)
}
@@ -64,29 +67,29 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
}
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
gui.State.ConflictTop = true
return gui.refreshMergePanel(g)
gui.State.Panels.Merging.ConflictTop = true
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
gui.State.ConflictTop = false
return gui.refreshMergePanel(g)
gui.State.Panels.Merging.ConflictTop = false
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.ConflictIndex >= len(gui.State.Conflicts)-1 {
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
return nil
}
gui.State.ConflictIndex++
return gui.refreshMergePanel(g)
gui.State.Panels.Merging.ConflictIndex++
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.ConflictIndex <= 0 {
if gui.State.Panels.Merging.ConflictIndex <= 0 {
return nil
}
gui.State.ConflictIndex--
return gui.refreshMergePanel(g)
gui.State.Panels.Merging.ConflictIndex--
return gui.refreshMergePanel()
}
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
@@ -133,101 +136,108 @@ func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
if err != nil {
return err
}
gui.State.EditHistory.Push(content)
gui.State.Panels.Merging.EditHistory.Push(content)
return nil
}
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
if gui.State.EditHistory.Len() == 0 {
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
return nil
}
prevContent := gui.State.EditHistory.Pop().(string)
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
return gui.refreshMergePanel(g)
return gui.refreshMergePanel()
}
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
gui.pushFileSnapshot(g)
pick := "bottom"
if gui.State.ConflictTop {
if gui.State.Panels.Merging.ConflictTop {
pick = "top"
}
err := gui.resolveConflict(g, conflict, pick)
if err != nil {
panic(err)
}
gui.refreshMergePanel(g)
return nil
// if that was the last conflict, finish the merge for this file
if len(gui.State.Panels.Merging.Conflicts) == 1 {
if err := gui.handleCompleteMerge(); err != nil {
return err
}
}
return gui.refreshMergePanel()
}
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
gui.pushFileSnapshot(g)
err := gui.resolveConflict(g, conflict, "both")
if err != nil {
panic(err)
}
return gui.refreshMergePanel(g)
return gui.refreshMergePanel()
}
func (gui *Gui) refreshMergePanel(g *gocui.Gui) error {
cat, err := gui.catSelectedFile(g)
func (gui *Gui) refreshMergePanel() error {
panelState := gui.State.Panels.Merging
cat, err := gui.catSelectedFile(gui.g)
if err != nil {
return err
}
if cat == "" {
return nil
}
gui.State.Conflicts, err = gui.findConflicts(cat)
panelState.Conflicts, err = gui.findConflicts(cat)
if err != nil {
return err
}
if len(gui.State.Conflicts) == 0 {
return gui.handleCompleteMerge(g)
} else if gui.State.ConflictIndex > len(gui.State.Conflicts)-1 {
gui.State.ConflictIndex = len(gui.State.Conflicts) - 1
// handle potential fixes that the user made in their editor since we last refreshed
if len(panelState.Conflicts) == 0 {
return gui.handleCompleteMerge()
} else if panelState.ConflictIndex > len(panelState.Conflicts)-1 {
panelState.ConflictIndex = len(panelState.Conflicts) - 1
}
hasFocus := gui.currentViewName(g) == "main"
content, err := gui.coloredConflictFile(cat, gui.State.Conflicts, gui.State.ConflictIndex, gui.State.ConflictTop, hasFocus)
hasFocus := gui.currentViewName() == "main"
content, err := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
if err != nil {
return err
}
if err := gui.scrollToConflict(g); err != nil {
if err := gui.renderString(gui.g, "main", content); err != nil {
return err
}
return gui.renderString(g, "main", content)
if err := gui.scrollToConflict(gui.g); err != nil {
return err
}
mainView := gui.getMainView()
mainView.Wrap = false
return nil
}
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
mainView, err := g.View("main")
if err != nil {
return err
}
if len(gui.State.Conflicts) == 0 {
panelState := gui.State.Panels.Merging
if len(panelState.Conflicts) == 0 {
return nil
}
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
ox, _ := mainView.Origin()
_, height := mainView.Size()
mergingView := gui.getMainView()
conflict := panelState.Conflicts[panelState.ConflictIndex]
ox, _ := mergingView.Origin()
_, height := mergingView.Size()
conflictMiddle := (conflict.End + conflict.Start) / 2
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
return mainView.SetOrigin(ox, newOriginY)
}
func (gui *Gui) switchToMerging(g *gocui.Gui) error {
gui.State.ConflictIndex = 0
gui.State.ConflictTop = true
_, err := g.SetCurrentView("main")
if err != nil {
return err
}
return gui.refreshMergePanel(g)
gui.g.Update(func(g *gocui.Gui) error {
return mergingView.SetOrigin(ox, newOriginY)
})
return nil
}
func (gui *Gui) renderMergeOptions() error {
@@ -241,20 +251,40 @@ func (gui *Gui) renderMergeOptions() error {
}
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
filesView, err := g.View("files")
if err != nil {
gui.State.Panels.Merging.EditHistory = stack.New()
if err := gui.refreshFiles(); err != nil {
return err
}
gui.refreshFiles(g)
return gui.switchFocus(g, v, filesView)
// it's possible this method won't be called from the merging view so we need to
// ensure we only 'return' focus if we already have it
if gui.g.CurrentView() == gui.getMainView() {
return gui.switchFocus(g, v, gui.getFilesView())
}
return nil
}
func (gui *Gui) handleCompleteMerge(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(gui.g); err != nil {
return err
}
gui.stageSelectedFile(g)
gui.refreshFiles(g)
return gui.switchFocus(g, nil, filesView)
if err := gui.refreshFiles(); err != nil {
return err
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.State.WorkingTreeState == "normal" {
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
if !gui.anyFilesWithMergeConflicts() {
return gui.promptToContinue()
}
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinue() error {
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error {
return gui.genericMergeCommand("continue")
}, nil)
}

View File

@@ -1,7 +1,9 @@
package gui
import (
"errors"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
)
@@ -11,7 +13,7 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
bindingsGlobal, bindingsPanel []*Binding
)
bindings := gui.GetKeybindings()
bindings := gui.GetCurrentKeybindings()
for _, binding := range bindings {
if binding.GetKey() != "" && binding.Description != "" {
@@ -47,5 +49,5 @@ func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
return bindings[index].Handler(g, v)
}
return gui.createMenu(bindings, handleMenuPress)
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), bindings, handleMenuPress)
}

View File

@@ -0,0 +1,91 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
type option struct {
value string
}
// GetDisplayStrings is a function.
func (r *option) GetDisplayStrings(isFocused bool) []string {
return []string{r.value}
}
func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error {
options := []*option{
{value: "continue"},
{value: "abort"},
}
if gui.State.WorkingTreeState == "rebasing" {
options = append(options, &option{value: "skip"})
}
handleMenuPress := func(index int) error {
command := options[index].value
return gui.genericMergeCommand(command)
}
var title string
if gui.State.WorkingTreeState == "merging" {
title = gui.Tr.SLocalize("MergeOptionsTitle")
} else {
title = gui.Tr.SLocalize("RebaseOptionsTitle")
}
return gui.createMenu(title, options, handleMenuPress)
}
func (gui *Gui) genericMergeCommand(command string) error {
status := gui.State.WorkingTreeState
if status != "merging" && status != "rebasing" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotMergingOrRebasing"))
}
commandType := strings.Replace(status, "ing", "e", 1)
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
if status == "merging" && command != "abort" && gui.Config.GetUserConfig().GetBool("git.merging.manualCommit") {
sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
return nil
}
result := gui.GitCommand.GenericMerge(commandType, command)
if err := gui.handleGenericMergeCommandResult(result); err != nil {
return err
}
return nil
}
func (gui *Gui) handleGenericMergeCommandResult(result error) error {
if err := gui.refreshSidePanels(gui.g); err != nil {
return err
}
if result == nil {
return nil
} else if result == gui.Errors.ErrSubProcess {
return result
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
return gui.genericMergeCommand("skip")
} else if strings.Contains(result.Error(), "When you have resolved this problem") || strings.Contains(result.Error(), "fix conflicts") || strings.Contains(result.Error(), "Resolve all conflicts manually") {
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), gui.Tr.SLocalize("FoundConflictsTitle"), gui.Tr.SLocalize("FoundConflicts"),
func(g *gocui.Gui, v *gocui.View) error {
return nil
}, func(g *gocui.Gui, v *gocui.View) error {
return gui.genericMergeCommand("abort")
},
)
} else {
return gui.createErrorPanel(gui.g, result.Error())
}
}

View File

@@ -15,7 +15,7 @@ type recentRepo struct {
}
// GetDisplayStrings returns the path from a recent repo.
func (r *recentRepo) GetDisplayStrings() []string {
func (r *recentRepo) GetDisplayStrings(isFocused bool) []string {
yellow := color.New(color.FgMagenta)
base := filepath.Base(r.path)
path := yellow.Sprint(r.path)
@@ -36,7 +36,7 @@ func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
if err := os.Chdir(repo.path); err != nil {
return err
}
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr)
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config)
if err != nil {
return err
}
@@ -44,7 +44,7 @@ func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
return gui.Errors.ErrSwitchRepo
}
return gui.createMenu(recentRepos, handleMenuPress)
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), recentRepos, handleMenuPress)
}
// updateRecentRepoList registers the fact that we opened lazygit in this repo,

View File

@@ -1,8 +1,6 @@
package gui
import (
"errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -60,23 +58,28 @@ func (gui *Gui) refreshStagingPanel() error {
}
if len(stageableLines) == 0 {
return errors.New("No lines to stage")
return gui.createErrorPanel(gui.g, "No lines to stage")
}
if err := gui.focusLineAndHunk(); err != nil {
return err
}
return gui.renderString(gui.g, "staging", colorDiff)
mainView := gui.getMainView()
mainView.Highlight = true
mainView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
})
return nil
}
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
if _, err := gui.g.SetViewOnBottom("staging"); err != nil {
return err
}
gui.State.Panels.Staging = nil
return gui.switchFocus(gui.g, nil, gui.getFilesView(gui.g))
return gui.switchFocus(gui.g, nil, gui.getFilesView())
}
func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error {
@@ -138,7 +141,7 @@ func (gui *Gui) handleCycleLine(prev bool) error {
// focusLineAndHunk works out the best focus for the staging panel given the
// selected line and size of the hunk
func (gui *Gui) focusLineAndHunk() error {
stagingView := gui.getStagingView(gui.g)
stagingView := gui.getMainView()
state := gui.State.Panels.Staging
lineNumber := state.StageableLines[state.SelectedLine]
@@ -209,7 +212,7 @@ func (gui *Gui) handleStageLineOrHunk(hunk bool) error {
return err
}
if err := gui.refreshFiles(gui.g); err != nil {
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.refreshStagingPanel(); err != nil {

View File

@@ -20,6 +20,13 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
}
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
stashEntry := gui.getSelectedStashEntry(v)
if stashEntry == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
@@ -41,31 +48,49 @@ func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
list, err := utils.RenderList(gui.State.StashEntries)
isFocused := gui.g.CurrentView().Name() == "stash"
list, err := utils.RenderList(gui.State.StashEntries, isFocused)
if err != nil {
return err
}
v := gui.getStashView(gui.g)
v := gui.getStashView()
v.Clear()
fmt.Fprint(v, list)
return gui.resetOrigin(v)
if err := gui.resetOrigin(v); err != nil {
return err
}
return nil
})
return nil
}
func (gui *Gui) handleStashNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleStashEntrySelect(gui.g, v)
}
func (gui *Gui) handleStashPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleStashEntrySelect(gui.g, v)
}
@@ -102,7 +127,7 @@ func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
gui.createErrorPanel(g, err.Error())
}
gui.refreshStashEntries(g)
return gui.refreshFiles(g)
return gui.refreshFiles()
}
func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
@@ -114,7 +139,7 @@ func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
gui.createErrorPanel(g, err.Error())
}
gui.refreshStashEntries(g)
return gui.refreshFiles(g)
return gui.refreshFiles()
})
return nil
}

View File

@@ -22,11 +22,11 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
pushables, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
branches := gui.State.Branches
if err := gui.updateHasMergeConflictStatus(); err != nil {
if err := gui.updateWorkTreeState(); err != nil {
return err
}
if gui.State.HasMergeConflicts {
fmt.Fprint(v, utils.ColoredString(" (merging)", color.FgYellow))
if gui.State.WorkingTreeState != "normal" {
fmt.Fprint(v, utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow))
}
if len(branches) == 0 {
@@ -44,11 +44,18 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return gui.createMessagePanel(gui.g, v, "", gui.Tr.SLocalize("CheckingForUpdates"))
return gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("CheckingForUpdates"))
}
func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
blue := color.New(color.FgBlue)
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
magenta := color.New(color.FgMagenta)
dashboardString := strings.Join(
[]string{
@@ -58,7 +65,7 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
"Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
"Tutorial: https://youtu.be/VDXvbHZYeKY",
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
blue.Sprint("Buy Jesse a coffee: https://donorbox.org/lazygit"), // caffeine ain't free
magenta.Sprint("Buy Jesse a coffee: https://donorbox.org/lazygit"), // caffeine ain't free
}, "\n\n")
return gui.renderString(g, "main", dashboardString)
@@ -84,3 +91,24 @@ func lazygitTitle() string {
__/ | __/ |
|___/ |___/ `
}
func (gui *Gui) updateWorkTreeState() error {
merging, err := gui.GitCommand.IsInMergeState()
if err != nil {
return err
}
if merging {
gui.State.WorkingTreeState = "merging"
return nil
}
rebaseMode, err := gui.GitCommand.RebaseMode()
if err != nil {
return err
}
if rebaseMode != "" {
gui.State.WorkingTreeState = "rebasing"
return nil
}
gui.State.WorkingTreeState = "normal"
return nil
}

View File

@@ -16,7 +16,7 @@ func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
if err := gui.refreshBranches(g); err != nil {
return err
}
if err := gui.refreshFiles(g); err != nil {
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.refreshCommits(g); err != nil {
@@ -104,13 +104,11 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
case "credentials":
return gui.handleCredentialsViewFocused(g, v)
case "main":
// TODO: pull this out into a 'view focused' function
gui.refreshMergePanel(g)
if gui.State.Contexts["main"] == "merging" {
return gui.refreshMergePanel()
}
v.Highlight = false
return nil
case "staging":
return nil
// return gui.handleStagingSelect(g, v)
default:
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
@@ -129,19 +127,11 @@ func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
}
// pass in oldView = nil if you don't want to be able to return to your old view
// TODO: move some of this logic into our onFocusLost and onFocus hooks
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
// we assume we'll never want to return focus to a confirmation panel i.e.
// we should never stack confirmation panels
if oldView != nil && oldView.Name() != "confirmation" {
oldView.Highlight = false
message := gui.Tr.TemplateLocalize(
"settingPreviewsViewTo",
Teml{
"oldViewName": oldView.Name(),
},
)
gui.Log.Info(message)
// second class panels should never have focus restored to them because
// once they lose focus they are effectively 'destroyed'
secondClassPanels := []string{"confirmation", "menu"}
@@ -150,7 +140,7 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
}
}
newView.Highlight = true
gui.Log.Info("setting highlight to true for view" + newView.Name())
message := gui.Tr.TemplateLocalize(
"newFocusedViewIs",
Teml{
@@ -263,38 +253,33 @@ func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
// TODO: refactor properly
// i'm so sorry but had to add this getBranchesView
func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View {
v, _ := g.View("files")
func (gui *Gui) getFilesView() *gocui.View {
v, _ := gui.g.View("files")
return v
}
func (gui *Gui) getCommitsView(g *gocui.Gui) *gocui.View {
v, _ := g.View("commits")
func (gui *Gui) getCommitsView() *gocui.View {
v, _ := gui.g.View("commits")
return v
}
func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View {
v, _ := g.View("commitMessage")
func (gui *Gui) getCommitMessageView() *gocui.View {
v, _ := gui.g.View("commitMessage")
return v
}
func (gui *Gui) getBranchesView(g *gocui.Gui) *gocui.View {
v, _ := g.View("branches")
func (gui *Gui) getBranchesView() *gocui.View {
v, _ := gui.g.View("branches")
return v
}
func (gui *Gui) getStagingView(g *gocui.Gui) *gocui.View {
v, _ := g.View("staging")
func (gui *Gui) getMainView() *gocui.View {
v, _ := gui.g.View("main")
return v
}
func (gui *Gui) getMainView(g *gocui.Gui) *gocui.View {
v, _ := g.View("main")
return v
}
func (gui *Gui) getStashView(g *gocui.Gui) *gocui.View {
v, _ := g.View("stash")
func (gui *Gui) getStashView() *gocui.View {
v, _ := gui.g.View("stash")
return v
}
@@ -302,8 +287,8 @@ func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
func (gui *Gui) currentViewName(g *gocui.Gui) string {
currentView := g.CurrentView()
func (gui *Gui) currentViewName() string {
currentView := gui.g.CurrentView()
return currentView.Name()
}
@@ -371,7 +356,8 @@ func (gui *Gui) refreshSelectedLine(line *int, total int) {
func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error {
gui.g.Update(func(g *gocui.Gui) error {
list, err := utils.RenderList(items)
isFocused := gui.g.CurrentView().Name() == v.Name()
list, err := utils.RenderList(items, isFocused)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
@@ -388,8 +374,26 @@ func (gui *Gui) renderPanelOptions() error {
case "menu":
return gui.renderMenuOptions()
case "main":
return gui.renderMergeOptions()
default:
return gui.renderGlobalOptions()
if gui.State.Contexts["main"] == "merging" {
return gui.renderMergeOptions()
}
}
return gui.renderGlobalOptions()
}
func (gui *Gui) handleFocusView(g *gocui.Gui, v *gocui.View) error {
_, err := gui.g.SetCurrentView(v.Name())
return err
}
func (gui *Gui) popupPanelFocused() bool {
viewNames := []string{"commitMessage",
"credentials",
"menu"}
for _, viewName := range viewNames {
if gui.currentViewName() == viewName {
return true
}
}
return false
}

View File

@@ -16,6 +16,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "DiffTitle",
Other: "Diff",
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Bestanden",
@@ -28,6 +31,12 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
}, &i18n.Message{
ID: "StagingMainTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Commit bericht",
@@ -181,6 +190,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Weet je zeker dat je branch {{.selectedBranchName}} geforceerd wil verwijderen?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "Je kan niet een branch in zichzelf mergen",
@@ -265,9 +277,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Geen commits voor deze branch",
}, &i18n.Message{
ID: "Error",
Other: "Foutmelding",
@@ -325,18 +334,12 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Er machen geen weergave met de newLineFocused switch declaratie",
}, &i18n.Message{
ID: "settingPreviewsViewTo",
Other: "vorige weergave instellen op: {{.oldViewName}}",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nieuw gefocussed weergave is {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Kon de bevestiging prompt niet sluiten: {{.error}}",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Geen veranderde files",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "maak bestandsvenster leeg",
@@ -432,7 +435,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: `Kan alleen individuele lijnen stagen van getrackte bestanden met onstaged veranderingen`,
}, &i18n.Message{
ID: "StagingTitle",
Other: `Staging`,
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "StageHunk",
Other: `stage hunk`,
@@ -448,6 +451,198 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CantFindHunk",
Other: `Kan geen hunk vinden`,
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Are you sure you want to rebase {{.checkedOutBranch}} onto {{.selectedBranch}}?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Cannot fast-forward a branch with no upstream",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Cannot fast-forward a branch with commits to push",
}, &i18n.Message{
ID: "MainTitle",
Other: "Main",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normal",
}, &i18n.Message{
ID: "softReset",
Other: "soft reset to last commit",
}, &i18n.Message{
ID: "SoftReset",
Other: "Soft reset",
}, &i18n.Message{
ID: "ConfirmSoftReset",
Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "You cannot rebase a branch onto itself",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Are you sure you want to squash this commit into the commit below?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "revert commit",
}, &i18n.Message{
ID: "deleteCommit",
Other: "delete commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "move commit down one",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "move commit up one",
}, &i18n.Message{
ID: "editCommit",
Other: "edit commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "amend commit with staged changes",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Damn, conflicts! To abort press 'esc', otherwise press 'enter'",
}, &i18n.Message{
ID: "FoundConflictsTitle",
Other: "Auto-merge failed",
}, &i18n.Message{
ID: "Undo",
Other: "undo",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "view merge/rebase options",
}, &i18n.Message{
ID: "NotMergingOrRebasing",
Other: "You are currently neither rebasing nor merging",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Options",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Options",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "all merge conflicts resolved. Continue?",
}, &i18n.Message{
ID: "NoRoom",
Other: "Not enough room",
}, &i18n.Message{
ID: "YouAreHere",
Other: "YOU ARE HERE",
}, &i18n.Message{
ID: "rewordNotSupported",
Other: "rewording commits while interactively rebasing is not currently supported",
}, &i18n.Message{
ID: "cherryPickCopy",
Other: "copy commit (cherry-pick)",
}, &i18n.Message{
ID: "cherryPickCopyRange",
Other: "copy commit range (cherry-pick)",
}, &i18n.Message{
ID: "pasteCommits",
Other: "paste commits (cherry-pick)",
}, &i18n.Message{
ID: "SureCherryPick",
Other: "Are you sure you want to cherry-pick the copied commits onto this branch?",
}, &i18n.Message{
ID: "CherryPick",
Other: "Cherry-Pick",
}, &i18n.Message{
ID: "CannotRebaseOntoFirstCommit",
Other: "You cannot interactive rebase onto the first commit",
}, &i18n.Message{
ID: "Donate",
Other: "Donate",
}, &i18n.Message{
ID: "PrevLine",
Other: "select previous line",
}, &i18n.Message{
ID: "NextLine",
Other: "select next line",
}, &i18n.Message{
ID: "PrevHunk",
Other: "select previous hunk",
}, &i18n.Message{
ID: "NextHunk",
Other: "select next hunk",
}, &i18n.Message{
ID: "PrevConflict",
Other: "select previous conflict",
}, &i18n.Message{
ID: "NextConflict",
Other: "select next conflict",
}, &i18n.Message{
ID: "SelectTop",
Other: "select top hunk",
}, &i18n.Message{
ID: "SelectBottom",
Other: "select bottom hunk",
}, &i18n.Message{
ID: "ScrollDown",
Other: "scroll down",
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll up",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Amend Commit",
}, &i18n.Message{
ID: "AmendCommitPrompt",
Other: "Are you sure you want to amend this commit with your staged files?",
}, &i18n.Message{
ID: "DeleteCommitTitle",
Other: "Delete Commit",
}, &i18n.Message{
ID: "DeleteCommitPrompt",
Other: "Are you sure you want to delete this commit?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
}, &i18n.Message{
ID: "DeletingStatus",
Other: "deleting",
}, &i18n.Message{
ID: "MovingStatus",
Other: "moving",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "amending",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
},
)
}

View File

@@ -24,6 +24,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "DiffTitle",
Other: "Diff",
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Files",
@@ -36,6 +39,24 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
}, &i18n.Message{
ID: "StagingMainTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
}, &i18n.Message{
ID: "MainTitle",
Other: "Main",
}, &i18n.Message{
ID: "StagingTitle",
Other: "Staging",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normal",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Commit message",
@@ -56,7 +77,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "amend last commit",
}, &i18n.Message{
ID: "SureToAmend",
Other: "Are you sure you want to amend last commit? You can change commit message from commits panel.",
Other: "Are you sure you want to amend last commit? Afterwards, you can change commit message from the commits panel.",
}, &i18n.Message{
ID: "NoCommitToAmend",
Other: "There's no commit to amend.",
@@ -81,6 +102,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "stashFiles",
Other: "stash files",
}, &i18n.Message{
ID: "softReset",
Other: "soft reset to last commit",
}, &i18n.Message{
ID: "open",
Other: "open",
@@ -155,10 +179,16 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "Fetching...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "This file has no merge conflicts",
Other: "This file has no inline merge conflicts",
}, &i18n.Message{
ID: "SureResetHardHead",
Other: "Are you sure you want `reset --hard HEAD` and `clean -fd`? You may lose changes",
Other: "Are you sure you want to `reset --hard HEAD` and `clean -fd`? You may lose changes",
}, &i18n.Message{
ID: "SoftReset",
Other: "Soft reset",
}, &i18n.Message{
ID: "ConfirmSoftReset",
Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree",
}, &i18n.Message{
ID: "SureTo",
Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?",
@@ -189,6 +219,12 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "{{.selectedBranchName}} is not fully merged. Are you sure you want to delete it?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "You cannot rebase a branch onto itself",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "You cannot merge a branch into itself",
@@ -260,13 +296,40 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "Fixup",
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one",
Other: "Are you sure you want to 'fixup' this commit? It will be merged into the commit below",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Are you sure you want to squash this commit into the commit below?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "revert commit",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Can only rename topmost commit",
Other: "Can only reword topmost commit from within lazygit. Use shift+R instead",
}, &i18n.Message{
ID: "renameCommit",
Other: "rename commit",
Other: "reword commit",
}, &i18n.Message{
ID: "deleteCommit",
Other: "delete commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "move commit down one",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "move commit up one",
}, &i18n.Message{
ID: "editCommit",
Other: "edit commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "amend commit with staged changes",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "rename commit with editor",
@@ -333,9 +396,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "No view matching newLineFocused switch statement",
}, &i18n.Message{
ID: "settingPreviewsViewTo",
Other: "setting previous view to: {{.oldViewName}}",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "new focused view is {{.newFocusedView}}",
@@ -438,9 +498,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "FileStagingRequirements",
Other: `Can only stage individual lines for tracked files with unstaged changes`,
}, &i18n.Message{
ID: "StagingTitle",
Other: `Staging`,
}, &i18n.Message{
ID: "StageHunk",
Other: `stage hunk`,
@@ -462,6 +519,153 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "Fetching",
Other: "fetching and fast-forwarding {{.from}} -> {{.to}} ...",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Damn, conflicts! To abort press 'esc', otherwise press 'enter'",
}, &i18n.Message{
ID: "FoundConflictsTitle",
Other: "Auto-merge failed",
}, &i18n.Message{
ID: "Undo",
Other: "undo",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "view merge/rebase options",
}, &i18n.Message{
ID: "NotMergingOrRebasing",
Other: "You are currently neither rebasing nor merging",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Options",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Options",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "all merge conflicts resolved. Continue?",
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Are you sure you want to rebase {{.checkedOutBranch}} onto {{.selectedBranch}}?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
}, &i18n.Message{}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Cannot fast-forward a branch with no upstream",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Cannot fast-forward a branch with commits to push",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "NoRoom",
Other: "Not enough room",
}, &i18n.Message{
ID: "YouAreHere",
Other: "YOU ARE HERE",
}, &i18n.Message{
ID: "rewordNotSupported",
Other: "rewording commits while interactively rebasing is not currently supported",
}, &i18n.Message{
ID: "cherryPickCopy",
Other: "copy commit (cherry-pick)",
}, &i18n.Message{
ID: "cherryPickCopyRange",
Other: "copy commit range (cherry-pick)",
}, &i18n.Message{
ID: "pasteCommits",
Other: "paste commits (cherry-pick)",
}, &i18n.Message{
ID: "SureCherryPick",
Other: "Are you sure you want to cherry-pick the copied commits onto this branch?",
}, &i18n.Message{
ID: "CherryPick",
Other: "Cherry-Pick",
}, &i18n.Message{
ID: "CannotRebaseOntoFirstCommit",
Other: "You cannot interactive rebase onto the first commit",
}, &i18n.Message{
ID: "Donate",
Other: "Donate",
}, &i18n.Message{
ID: "PrevLine",
Other: "select previous line",
}, &i18n.Message{
ID: "NextLine",
Other: "select next line",
}, &i18n.Message{
ID: "PrevHunk",
Other: "select previous hunk",
}, &i18n.Message{
ID: "NextHunk",
Other: "select next hunk",
}, &i18n.Message{
ID: "PrevConflict",
Other: "select previous conflict",
}, &i18n.Message{
ID: "NextConflict",
Other: "select next conflict",
}, &i18n.Message{
ID: "SelectTop",
Other: "select top hunk",
}, &i18n.Message{
ID: "SelectBottom",
Other: "select bottom hunk",
}, &i18n.Message{
ID: "ScrollDown",
Other: "scroll down",
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll up",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Amend Commit",
}, &i18n.Message{
ID: "AmendCommitPrompt",
Other: "Are you sure you want to amend this commit with your staged files?",
}, &i18n.Message{
ID: "DeleteCommitTitle",
Other: "Delete Commit",
}, &i18n.Message{
ID: "DeleteCommitPrompt",
Other: "Are you sure you want to delete this commit?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
}, &i18n.Message{
ID: "DeletingStatus",
Other: "deleting",
}, &i18n.Message{
ID: "MovingStatus",
Other: "moving",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "amending",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
},
)
}

View File

@@ -14,6 +14,9 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "DiffTitle",
Other: "Różnice",
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Pliki",
@@ -26,6 +29,12 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "StashTitle",
Other: "Schowek",
}, &i18n.Message{
ID: "StagingMainTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Wiadomość commita",
@@ -170,6 +179,12 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Na pewno wymusić usunięcie gałęzi {{.selectedBranchName}}?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "You cannot rebase a branch onto itself",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "Nie możesz scalić gałęzi do samej siebie",
@@ -254,9 +269,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "potencjalny błąd w getSelected Commit (niedopasowane ui i stan)",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Brak commitów dla tej gałęzi",
}, &i18n.Message{
ID: "Error",
Other: "Błąd",
@@ -314,18 +326,12 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Brak widoku pasującego do instrukcji przełączania newLineFocused",
}, &i18n.Message{
ID: "settingPreviewsViewTo",
Other: "ustawianie poprzedniego widoku na: {{.oldViewName}}",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nowy skupiony widok to {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Nie można zamknąć monitu potwierdzenia: {{.error}}",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Brak zmienionych plików",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "Wyczyść panel plików",
@@ -431,6 +437,195 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CantFindHunk",
Other: `Nie można znaleźć kawałka`,
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Are you sure you want to rebase {{.checkedOutBranch}} onto {{.selectedBranch}}?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
}, &i18n.Message{}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Cannot fast-forward a branch with no upstream",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Cannot fast-forward a branch with commits to push",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "MainTitle",
Other: "Main",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normal",
}, &i18n.Message{
ID: "softReset",
Other: "soft reset to last commit",
}, &i18n.Message{
ID: "SoftReset",
Other: "Soft reset",
}, &i18n.Message{
ID: "ConfirmSoftReset",
Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Are you sure you want to squash this commit into the commit below?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "revert commit",
}, &i18n.Message{
ID: "deleteCommit",
Other: "delete commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "move commit down one",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "move commit up one",
}, &i18n.Message{
ID: "editCommit",
Other: "edit commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "amend commit with staged changes",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Damn, conflicts! To abort press 'esc', otherwise press 'enter'",
}, &i18n.Message{
ID: "FoundConflictsTitle",
Other: "Auto-merge failed",
}, &i18n.Message{
ID: "Undo",
Other: "undo",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "view merge/rebase options",
}, &i18n.Message{
ID: "NotMergingOrRebasing",
Other: "You are currently neither rebasing nor merging",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Options",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Options",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "all merge conflicts resolved. Continue?",
}, &i18n.Message{
ID: "NoRoom",
Other: "Not enough room",
}, &i18n.Message{
ID: "YouAreHere",
Other: "YOU ARE HERE",
}, &i18n.Message{
ID: "rewordNotSupported",
Other: "rewording commits while interactively rebasing is not currently supported",
}, &i18n.Message{
ID: "cherryPickCopy",
Other: "copy commit (cherry-pick)",
}, &i18n.Message{
ID: "cherryPickCopyRange",
Other: "copy commit range (cherry-pick)",
}, &i18n.Message{
ID: "pasteCommits",
Other: "paste commits (cherry-pick)",
}, &i18n.Message{
ID: "SureCherryPick",
Other: "Are you sure you want to cherry-pick the copied commits onto this branch?",
}, &i18n.Message{
ID: "CherryPick",
Other: "Cherry-Pick",
}, &i18n.Message{
ID: "CannotRebaseOntoFirstCommit",
Other: "You cannot interactive rebase onto the first commit",
}, &i18n.Message{
ID: "Donate",
Other: "Donate",
}, &i18n.Message{
ID: "PrevLine",
Other: "select previous line",
}, &i18n.Message{
ID: "NextLine",
Other: "select next line",
}, &i18n.Message{
ID: "PrevHunk",
Other: "select previous hunk",
}, &i18n.Message{
ID: "NextHunk",
Other: "select next hunk",
}, &i18n.Message{
ID: "PrevConflict",
Other: "select previous conflict",
}, &i18n.Message{
ID: "NextConflict",
Other: "select next conflict",
}, &i18n.Message{
ID: "SelectTop",
Other: "select top hunk",
}, &i18n.Message{
ID: "SelectBottom",
Other: "select bottom hunk",
}, &i18n.Message{
ID: "ScrollDown",
Other: "scroll down",
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll up",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Amend Commit",
}, &i18n.Message{
ID: "AmendCommitPrompt",
Other: "Are you sure you want to amend this commit with your staged files?",
}, &i18n.Message{
ID: "DeleteCommitTitle",
Other: "Delete Commit",
}, &i18n.Message{
ID: "DeleteCommitPrompt",
Other: "Are you sure you want to delete this commit?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
}, &i18n.Message{
ID: "DeletingStatus",
Other: "deleting",
}, &i18n.Message{
ID: "MovingStatus",
Other: "moving",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "amending",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
},
)
}

View File

@@ -1,7 +1,7 @@
package test
import (
"errors"
"github.com/go-errors/errors"
"os"
"os/exec"
"path/filepath"

45
pkg/test/utils.go Normal file
View File

@@ -0,0 +1,45 @@
package test
import (
"fmt"
"os/exec"
"strings"
"testing"
"github.com/mgutz/str"
"github.com/stretchr/testify/assert"
)
// CommandSwapper takes a command, verifies that it is what it's expected to be
// and then returns a replacement command that will actually be called by the os
type CommandSwapper struct {
Expect string
Replace string
}
// SwapCommand verifies the command is what we expected, and swaps it out for a different command
func (i *CommandSwapper) SwapCommand(t *testing.T, cmd string, args []string) *exec.Cmd {
splitCmd := str.ToArgv(i.Expect)
assert.EqualValues(t, splitCmd[0], cmd, fmt.Sprintf("received command: %s %s", cmd, strings.Join(args, " ")))
if len(splitCmd) > 1 {
assert.EqualValues(t, splitCmd[1:], args, fmt.Sprintf("received command: %s %s", cmd, strings.Join(args, " ")))
}
splitCmd = str.ToArgv(i.Replace)
return exec.Command(splitCmd[0], splitCmd[1:]...)
}
// CreateMockCommand creates a command function that will verify its receiving the right sequence of commands from lazygit
func CreateMockCommand(t *testing.T, swappers []*CommandSwapper) func(cmd string, args ...string) *exec.Cmd {
commandIndex := 0
return func(cmd string, args ...string) *exec.Cmd {
var command *exec.Cmd
if commandIndex > len(swappers)-1 {
assert.Fail(t, fmt.Sprintf("too many commands run. This command was (%s %s)", cmd, strings.Join(args, " ")))
}
command = swappers[commandIndex].SwapCommand(t, cmd, args)
commandIndex++
return command
}
}

View File

@@ -2,7 +2,6 @@ package updates
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
@@ -13,6 +12,8 @@ import (
"strings"
"time"
"github.com/go-errors/errors"
"github.com/kardianos/osext"
getter "github.com/jesseduffield/go-getter"

View File

@@ -2,15 +2,17 @@ package utils
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/fatih/color"
)
@@ -112,13 +114,13 @@ func Min(x, y int) int {
}
type Displayable interface {
GetDisplayStrings() []string
GetDisplayStrings(bool) []string
}
// RenderList takes a slice of items, confirms they implement the Displayable
// interface, then generates a list of their displaystrings to write to a panel's
// buffer
func RenderList(slice interface{}) (string, error) {
func RenderList(slice interface{}, isFocused bool) (string, error) {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
return "", errors.New("RenderList given a non-slice type")
@@ -134,19 +136,19 @@ func RenderList(slice interface{}) (string, error) {
displayables[i] = value
}
return renderDisplayableList(displayables)
return renderDisplayableList(displayables, isFocused)
}
// renderDisplayableList takes a list of displayable items, obtains their display
// strings via GetDisplayStrings() and then returns a single string containing
// each item's string representation on its own line, with appropriate horizontal
// padding between the item's own strings
func renderDisplayableList(items []Displayable) (string, error) {
func renderDisplayableList(items []Displayable, isFocused bool) (string, error) {
if len(items) == 0 {
return "", nil
}
stringArrays := getDisplayStringArrays(items)
stringArrays := getDisplayStringArrays(items, isFocused)
if !displayArraysAligned(stringArrays) {
return "", errors.New("Each item must return the same number of strings to display")
@@ -158,6 +160,12 @@ func renderDisplayableList(items []Displayable) (string, error) {
return strings.Join(paddedDisplayStrings, "\n"), nil
}
// Decolorise strips a string of color
func Decolorise(str string) string {
re := regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`)
return re.ReplaceAllString(str, "")
}
func getPadWidths(stringArrays [][]string) []int {
if len(stringArrays[0]) <= 1 {
return []int{}
@@ -165,8 +173,9 @@ func getPadWidths(stringArrays [][]string) []int {
padWidths := make([]int, len(stringArrays[0])-1)
for i := range padWidths {
for _, strings := range stringArrays {
if len(strings[i]) > padWidths[i] {
padWidths[i] = len(strings[i])
uncoloredString := Decolorise(strings[i])
if len(uncoloredString) > padWidths[i] {
padWidths[i] = len(uncoloredString)
}
}
}
@@ -198,10 +207,10 @@ func displayArraysAligned(stringArrays [][]string) bool {
return true
}
func getDisplayStringArrays(displayables []Displayable) [][]string {
func getDisplayStringArrays(displayables []Displayable, isFocused bool) [][]string {
stringArrays := make([][]string, len(displayables))
for i, item := range displayables {
stringArrays[i] = item.GetDisplayStrings()
stringArrays[i] = item.GetDisplayStrings(isFocused)
}
return stringArrays
}

View File

@@ -1,7 +1,6 @@
package utils
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
@@ -204,15 +203,19 @@ type myDisplayable struct {
type myStruct struct{}
// GetDisplayStrings is a function.
func (d *myDisplayable) GetDisplayStrings() []string {
func (d *myDisplayable) GetDisplayStrings(isFocused bool) []string {
if isFocused {
return append(d.strings, "blah")
}
return d.strings
}
// TestGetDisplayStringArrays is a function.
func TestGetDisplayStringArrays(t *testing.T) {
type scenario struct {
input []Displayable
expected [][]string
input []Displayable
isFocused bool
expected [][]string
}
scenarios := []scenario{
@@ -221,21 +224,31 @@ func TestGetDisplayStringArrays(t *testing.T) {
Displayable(&myDisplayable{[]string{"a", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
false,
[][]string{{"a", "b"}, {"c", "d"}},
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
true,
[][]string{{"a", "b", "blah"}, {"c", "d", "blah"}},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getDisplayStringArrays(s.input))
assert.EqualValues(t, s.expected, getDisplayStringArrays(s.input, s.isFocused))
}
}
// TestRenderDisplayableList is a function.
func TestRenderDisplayableList(t *testing.T) {
type scenario struct {
input []Displayable
expectedString string
expectedError error
input []Displayable
isFocused bool
expectedString string
expectedErrorMessage string
}
scenarios := []scenario{
@@ -244,40 +257,57 @@ func TestRenderDisplayableList(t *testing.T) {
Displayable(&myDisplayable{[]string{}}),
Displayable(&myDisplayable{[]string{}}),
},
false,
"\n",
nil,
"",
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"aa", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
false,
"aa b\nc d",
nil,
"",
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b", "c"}}),
},
false,
"",
"Each item must return the same number of strings to display",
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b"}}),
},
true,
"a blah\nb blah",
"",
errors.New("Each item must return the same number of strings to display"),
},
}
for _, s := range scenarios {
str, err := renderDisplayableList(s.input)
str, err := renderDisplayableList(s.input, s.isFocused)
assert.EqualValues(t, s.expectedString, str)
assert.EqualValues(t, s.expectedError, err)
if s.expectedErrorMessage != "" {
assert.EqualError(t, err, s.expectedErrorMessage)
} else {
assert.NoError(t, err)
}
}
}
// TestRenderList is a function.
func TestRenderList(t *testing.T) {
type scenario struct {
input interface{}
expectedString string
expectedError error
input interface{}
isFocused bool
expectedString string
expectedErrorMessage string
}
scenarios := []scenario{
@@ -286,28 +316,43 @@ func TestRenderList(t *testing.T) {
{[]string{"aa", "b"}},
{[]string{"c", "d"}},
},
false,
"aa b\nc d",
nil,
"",
},
{
[]*myStruct{
{},
{},
},
false,
"",
errors.New("item does not implement the Displayable interface"),
"item does not implement the Displayable interface",
},
{
&myStruct{},
false,
"",
"RenderList given a non-slice type",
},
{
[]*myDisplayable{
{[]string{"a"}},
},
true,
"a blah",
"",
errors.New("RenderList given a non-slice type"),
},
}
for _, s := range scenarios {
str, err := RenderList(s.input)
str, err := RenderList(s.input, s.isFocused)
assert.EqualValues(t, s.expectedString, str)
assert.EqualValues(t, s.expectedError, err)
if s.expectedErrorMessage != "" {
assert.EqualError(t, err, s.expectedErrorMessage)
} else {
assert.NoError(t, err)
}
}
}

View File

@@ -10,13 +10,33 @@ package main
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"log"
"os"
"strings"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui"
)
type bindingSection struct {
title string
bindings []*gui.Binding
}
func main() {
mConfig, _ := config.NewAppConfig("", "", "", "", "", true)
mApp, _ := app.NewApp(mConfig)
lang := mApp.Tr.GetLanguage()
file, _ := os.Create("Keybindings_" + lang + ".md")
bindingSections := getBindingSections(mApp)
content := formatSections(mApp, bindingSections)
writeString(file, content)
}
func writeString(file *os.File, str string) {
_, err := file.WriteString(str)
if err != nil {
@@ -24,40 +44,83 @@ func writeString(file *os.File, str string) {
}
}
func getTitle(mApp *app.App, viewName string) string {
viewTitle := strings.Title(viewName) + "Title"
translatedTitle := mApp.Tr.SLocalize(viewTitle)
formattedTitle := fmt.Sprintf("\n## %s\n\n", translatedTitle)
return formattedTitle
func localisedTitle(mApp *app.App, str string) string {
viewTitle := strings.Title(str) + "Title"
return mApp.Tr.SLocalize(viewTitle)
}
func main() {
mConfig, _ := config.NewAppConfig("", "", "", "", "", new(bool))
mApp, _ := app.Setup(mConfig)
lang := mApp.Tr.GetLanguage()
file, _ := os.Create("Keybindings_" + lang + ".md")
current := ""
func formatTitle(title string) string {
return fmt.Sprintf("\n## %s\n\n", title)
}
writeString(file, fmt.Sprintf("# Lazygit %s\n", mApp.Tr.SLocalize("menu")))
writeString(file, getTitle(mApp, "global"))
func formatBinding(binding *gui.Binding) string {
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", binding.GetKey(), binding.Description)
}
writeString(file, "<pre>\n")
func getBindingSections(mApp *app.App) []*bindingSection {
bindingSections := []*bindingSection{}
for _, binding := range mApp.Gui.GetKeybindings() {
// TODO: add context-based keybindings
for _, binding := range mApp.Gui.GetInitialKeybindings() {
if binding.Description == "" {
continue
}
if binding.ViewName != current {
current = binding.ViewName
writeString(file, "</pre>\n")
writeString(file, getTitle(mApp, current))
writeString(file, "<pre>\n")
viewName := binding.ViewName
if viewName == "" {
viewName = "global"
}
title := localisedTitle(mApp, viewName)
info := fmt.Sprintf(" <kbd>%s</kbd>: %s\n", binding.GetKey(), binding.Description)
writeString(file, info)
bindingSections = addBinding(title, bindingSections, binding)
}
writeString(file, "</pre>\n")
for view, contexts := range mApp.Gui.GetContextMap() {
for contextName, contextBindings := range contexts {
translatedView := localisedTitle(mApp, view)
translatedContextName := localisedTitle(mApp, contextName)
title := fmt.Sprintf("%s (%s)", translatedView, translatedContextName)
for _, binding := range contextBindings {
bindingSections = addBinding(title, bindingSections, binding)
}
}
}
return bindingSections
}
func addBinding(title string, bindingSections []*bindingSection, binding *gui.Binding) []*bindingSection {
if binding.Description == "" {
return bindingSections
}
for _, section := range bindingSections {
if title == section.title {
section.bindings = append(section.bindings, binding)
return bindingSections
}
}
section := &bindingSection{
title: title,
bindings: []*gui.Binding{binding},
}
return append(bindingSections, section)
}
func formatSections(mApp *app.App, bindingSections []*bindingSection) string {
content := fmt.Sprintf("# Lazygit %s\n", mApp.Tr.SLocalize("menu"))
for _, section := range bindingSections {
content += formatTitle(section.title)
content += "<pre>\n"
for _, binding := range section.bindings {
content += formatBinding(binding)
}
content += "</pre>\n"
}
return content
}

12
test.sh
View File

@@ -3,9 +3,19 @@
set -e
echo "" > coverage.txt
use_go_test=false
if command -v gotest; then
use_go_test=true
fi
for d in $( find ./* -maxdepth 10 ! -path "./vendor*" ! -path "./.git*" ! -path "./scripts*" -type d); do
if ls $d/*.go &> /dev/null; then
go test -v -race -coverprofile=profile.out -covermode=atomic $d
args="-v -race -coverprofile=profile.out -covermode=atomic $d"
if [ "$use_go_test" == true ]; then
gotest $args
else
go test $args
fi
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
set -ex; rm -rf repo; mkdir repo; cd repo
git init
@@ -24,30 +24,113 @@ git add file1
git add directory
git commit -m "first commit"
git checkout -b develop
git checkout -b feature/cherry-picking
echo "this is file number 1 that I'm going to cherry-pick" > cherrypicking1
echo "this is file number 2 that I'm going to cherry-pick" > cherrypicking2
git add .
git commit -am "first commit freshman year"
echo "this is file number 3 that I'm going to cherry-pick" > cherrypicking3
git add .
git commit -am "second commit subway eat fresh"
echo "this is file number 4 that I'm going to cherry-pick" > cherrypicking4
git add .
git commit -am "third commit fresh"
echo "this is file number 5 that I'm going to cherry-pick" > cherrypicking5
git add .
git commit -am "fourth commit cool"
echo "this is file number 6 that I'm going to cherry-pick" > cherrypicking6
git add .
git commit -am "fifth commit nice"
echo "this is file number 7 that I'm going to cherry-pick" > cherrypicking7
git add .
git commit -am "sixth commit haha"
echo "this is file number 8 that I'm going to cherry-pick" > cherrypicking8
git add .
git commit -am "seventh commit yeah"
echo "this is file number 9 that I'm going to cherry-pick" > cherrypicking9
git add .
git commit -am "eighth commit woo"
git checkout -b develop
echo "once upon a time there was a dog" >> file1
add_spacing file1
echo "once upon a time there was another dog" >> file1
git add file1
echo "test2" > directory/file
echo "test2" > directory/file2
git add directory
git commit -m "first commit on develop"
git checkout master
git checkout master
echo "once upon a time there was a cat" >> file1
add_spacing file1
echo "once upon a time there was another cat" >> file1
git add file1
echo "test3" > directory/file
echo "test3" > directory/file2
git add directory
git commit -m "first commit on master"
git commit -m "first commit on develop"
git merge develop # should have a merge conflict here
git checkout develop
echo "once upon a time there was a mouse" >> file3
git add file3
git commit -m "second commit on develop"
git checkout master
echo "once upon a time there was a horse" >> file3
git add file3
git commit -m "second commit on master"
git checkout develop
echo "once upon a time there was a mouse" >> file4
git add file4
git commit -m "third commit on develop"
git checkout master
echo "once upon a time there was a horse" >> file4
git add file4
git commit -m "third commit on master"
git checkout develop
echo "once upon a time there was a mouse" >> file5
git add file5
git commit -m "fourth commit on develop"
git checkout master
echo "once upon a time there was a horse" >> file5
git add file5
git commit -m "fourth commit on master"
# git merge develop # should have a merge conflict here

7
vendor/github.com/go-errors/errors/LICENSE.MIT generated vendored Normal file
View File

@@ -0,0 +1,7 @@
Copyright (c) 2015 Conrad Irwin <conrad@bugsnag.com>
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.

217
vendor/github.com/go-errors/errors/error.go generated vendored Normal file
View File

@@ -0,0 +1,217 @@
// Package errors provides errors that have stack-traces.
//
// This is particularly useful when you want to understand the
// state of execution when an error was returned unexpectedly.
//
// It provides the type *Error which implements the standard
// golang error interface, so you can use this library interchangably
// with code that is expecting a normal error return.
//
// For example:
//
// package crashy
//
// import "github.com/go-errors/errors"
//
// var Crashed = errors.Errorf("oh dear")
//
// func Crash() error {
// return errors.New(Crashed)
// }
//
// This can be called as follows:
//
// package main
//
// import (
// "crashy"
// "fmt"
// "github.com/go-errors/errors"
// )
//
// func main() {
// err := crashy.Crash()
// if err != nil {
// if errors.Is(err, crashy.Crashed) {
// fmt.Println(err.(*errors.Error).ErrorStack())
// } else {
// panic(err)
// }
// }
// }
//
// This package was original written to allow reporting to Bugsnag,
// but after I found similar packages by Facebook and Dropbox, it
// was moved to one canonical location so everyone can benefit.
package errors
import (
"bytes"
"fmt"
"reflect"
"runtime"
)
// The maximum number of stackframes on any error.
var MaxStackDepth = 50
// Error is an error with an attached stacktrace. It can be used
// wherever the builtin error interface is expected.
type Error struct {
Err error
stack []uintptr
frames []StackFrame
prefix string
}
// New makes an Error from the given value. If that value is already an
// error then it will be used directly, if not, it will be passed to
// fmt.Errorf("%v"). The stacktrace will point to the line of code that
// called New.
func New(e interface{}) *Error {
var err error
switch e := e.(type) {
case error:
err = e
default:
err = fmt.Errorf("%v", e)
}
stack := make([]uintptr, MaxStackDepth)
length := runtime.Callers(2, stack[:])
return &Error{
Err: err,
stack: stack[:length],
}
}
// Wrap makes an Error from the given value. If that value is already an
// error then it will be used directly, if not, it will be passed to
// fmt.Errorf("%v"). The skip parameter indicates how far up the stack
// to start the stacktrace. 0 is from the current call, 1 from its caller, etc.
func Wrap(e interface{}, skip int) *Error {
var err error
switch e := e.(type) {
case *Error:
return e
case error:
err = e
default:
err = fmt.Errorf("%v", e)
}
stack := make([]uintptr, MaxStackDepth)
length := runtime.Callers(2+skip, stack[:])
return &Error{
Err: err,
stack: stack[:length],
}
}
// WrapPrefix makes an Error from the given value. If that value is already an
// error then it will be used directly, if not, it will be passed to
// fmt.Errorf("%v"). The prefix parameter is used to add a prefix to the
// error message when calling Error(). The skip parameter indicates how far
// up the stack to start the stacktrace. 0 is from the current call,
// 1 from its caller, etc.
func WrapPrefix(e interface{}, prefix string, skip int) *Error {
err := Wrap(e, 1+skip)
if err.prefix != "" {
prefix = fmt.Sprintf("%s: %s", prefix, err.prefix)
}
return &Error{
Err: err.Err,
stack: err.stack,
prefix: prefix,
}
}
// Is detects whether the error is equal to a given error. Errors
// are considered equal by this function if they are the same object,
// or if they both contain the same error inside an errors.Error.
func Is(e error, original error) bool {
if e == original {
return true
}
if e, ok := e.(*Error); ok {
return Is(e.Err, original)
}
if original, ok := original.(*Error); ok {
return Is(e, original.Err)
}
return false
}
// Errorf creates a new error with the given message. You can use it
// as a drop-in replacement for fmt.Errorf() to provide descriptive
// errors in return values.
func Errorf(format string, a ...interface{}) *Error {
return Wrap(fmt.Errorf(format, a...), 1)
}
// Error returns the underlying error's message.
func (err *Error) Error() string {
msg := err.Err.Error()
if err.prefix != "" {
msg = fmt.Sprintf("%s: %s", err.prefix, msg)
}
return msg
}
// Stack returns the callstack formatted the same way that go does
// in runtime/debug.Stack()
func (err *Error) Stack() []byte {
buf := bytes.Buffer{}
for _, frame := range err.StackFrames() {
buf.WriteString(frame.String())
}
return buf.Bytes()
}
// Callers satisfies the bugsnag ErrorWithCallerS() interface
// so that the stack can be read out.
func (err *Error) Callers() []uintptr {
return err.stack
}
// ErrorStack returns a string that contains both the
// error message and the callstack.
func (err *Error) ErrorStack() string {
return err.TypeName() + " " + err.Error() + "\n" + string(err.Stack())
}
// StackFrames returns an array of frames containing information about the
// stack.
func (err *Error) StackFrames() []StackFrame {
if err.frames == nil {
err.frames = make([]StackFrame, len(err.stack))
for i, pc := range err.stack {
err.frames[i] = NewStackFrame(pc)
}
}
return err.frames
}
// TypeName returns the type this error. e.g. *errors.stringError.
func (err *Error) TypeName() string {
if _, ok := err.Err.(uncaughtPanic); ok {
return "panic"
}
return reflect.TypeOf(err.Err).String()
}

127
vendor/github.com/go-errors/errors/parse_panic.go generated vendored Normal file
View File

@@ -0,0 +1,127 @@
package errors
import (
"strconv"
"strings"
)
type uncaughtPanic struct{ message string }
func (p uncaughtPanic) Error() string {
return p.message
}
// ParsePanic allows you to get an error object from the output of a go program
// that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap.
func ParsePanic(text string) (*Error, error) {
lines := strings.Split(text, "\n")
state := "start"
var message string
var stack []StackFrame
for i := 0; i < len(lines); i++ {
line := lines[i]
if state == "start" {
if strings.HasPrefix(line, "panic: ") {
message = strings.TrimPrefix(line, "panic: ")
state = "seek"
} else {
return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line)
}
} else if state == "seek" {
if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "[running]:") {
state = "parsing"
}
} else if state == "parsing" {
if line == "" {
state = "done"
break
}
createdBy := false
if strings.HasPrefix(line, "created by ") {
line = strings.TrimPrefix(line, "created by ")
createdBy = true
}
i++
if i >= len(lines) {
return nil, Errorf("bugsnag.panicParser: Invalid line (unpaired): %s", line)
}
frame, err := parsePanicFrame(line, lines[i], createdBy)
if err != nil {
return nil, err
}
stack = append(stack, *frame)
if createdBy {
state = "done"
break
}
}
}
if state == "done" || state == "parsing" {
return &Error{Err: uncaughtPanic{message}, frames: stack}, nil
}
return nil, Errorf("could not parse panic: %v", text)
}
// The lines we're passing look like this:
//
// main.(*foo).destruct(0xc208067e98)
// /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151
func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) {
idx := strings.LastIndex(name, "(")
if idx == -1 && !createdBy {
return nil, Errorf("bugsnag.panicParser: Invalid line (no call): %s", name)
}
if idx != -1 {
name = name[:idx]
}
pkg := ""
if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 {
pkg += name[:lastslash] + "/"
name = name[lastslash+1:]
}
if period := strings.Index(name, "."); period >= 0 {
pkg += name[:period]
name = name[period+1:]
}
name = strings.Replace(name, "·", ".", -1)
if !strings.HasPrefix(line, "\t") {
return nil, Errorf("bugsnag.panicParser: Invalid line (no tab): %s", line)
}
idx = strings.LastIndex(line, ":")
if idx == -1 {
return nil, Errorf("bugsnag.panicParser: Invalid line (no line number): %s", line)
}
file := line[1:idx]
number := line[idx+1:]
if idx = strings.Index(number, " +"); idx > -1 {
number = number[:idx]
}
lno, err := strconv.ParseInt(number, 10, 32)
if err != nil {
return nil, Errorf("bugsnag.panicParser: Invalid line (bad line number): %s", line)
}
return &StackFrame{
File: file,
LineNumber: int(lno),
Package: pkg,
Name: name,
}, nil
}

102
vendor/github.com/go-errors/errors/stackframe.go generated vendored Normal file
View File

@@ -0,0 +1,102 @@
package errors
import (
"bytes"
"fmt"
"io/ioutil"
"runtime"
"strings"
)
// A StackFrame contains all necessary information about to generate a line
// in a callstack.
type StackFrame struct {
// The path to the file containing this ProgramCounter
File string
// The LineNumber in that file
LineNumber int
// The Name of the function that contains this ProgramCounter
Name string
// The Package that contains this function
Package string
// The underlying ProgramCounter
ProgramCounter uintptr
}
// NewStackFrame popoulates a stack frame object from the program counter.
func NewStackFrame(pc uintptr) (frame StackFrame) {
frame = StackFrame{ProgramCounter: pc}
if frame.Func() == nil {
return
}
frame.Package, frame.Name = packageAndName(frame.Func())
// pc -1 because the program counters we use are usually return addresses,
// and we want to show the line that corresponds to the function call
frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1)
return
}
// Func returns the function that contained this frame.
func (frame *StackFrame) Func() *runtime.Func {
if frame.ProgramCounter == 0 {
return nil
}
return runtime.FuncForPC(frame.ProgramCounter)
}
// String returns the stackframe formatted in the same way as go does
// in runtime/debug.Stack()
func (frame *StackFrame) String() string {
str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter)
source, err := frame.SourceLine()
if err != nil {
return str
}
return str + fmt.Sprintf("\t%s: %s\n", frame.Name, source)
}
// SourceLine gets the line of code (from File and Line) of the original source if possible.
func (frame *StackFrame) SourceLine() (string, error) {
data, err := ioutil.ReadFile(frame.File)
if err != nil {
return "", New(err)
}
lines := bytes.Split(data, []byte{'\n'})
if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) {
return "???", nil
}
// -1 because line-numbers are 1 based, but our array is 0 based
return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil
}
func packageAndName(fn *runtime.Func) (string, string) {
name := fn.Name()
pkg := ""
// The name includes the path name to the package, which is unnecessary
// since the file name is already included. Plus, it has center dots.
// That is, we see
// runtime/debug.*T·ptrmethod
// and want
// *T.ptrmethod
// Since the package path might contains dots (e.g. code.google.com/...),
// we first remove the path prefix if there is one.
if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 {
pkg += name[:lastslash] + "/"
name = name[lastslash+1:]
}
if period := strings.Index(name, "."); period >= 0 {
pkg += name[:period]
name = name[period+1:]
}
name = strings.Replace(name, "·", ".", -1)
return pkg, name
}

View File

@@ -5,7 +5,7 @@
package gocui
import (
"errors"
"github.com/go-errors/errors"
"github.com/mattn/go-runewidth"
)

View File

@@ -5,7 +5,7 @@
package gocui
import (
"errors"
"github.com/go-errors/errors"
"strconv"
)

View File

@@ -5,17 +5,20 @@
package gocui
import (
"errors"
standardErrors "errors"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/termbox-go"
)
var (
// ErrQuit is used to decide if the MainLoop finished successfully.
ErrQuit = errors.New("quit")
ErrQuit = standardErrors.New("quit")
// ErrUnknownView allows to assert if a View must be initialized.
ErrUnknownView = errors.New("unknown view")
ErrUnknownView = standardErrors.New("unknown view")
)
// OutputMode represents the terminal's output mode (8 or 256 colors).
@@ -46,6 +49,7 @@ type Gui struct {
keybindings []*keybinding
maxX, maxY int
outputMode OutputMode
stop chan struct{}
// BgColor and FgColor allow to configure the background and foreground
// colors of the GUI.
@@ -89,6 +93,8 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) {
g.outputMode = mode
termbox.SetOutputMode(termbox.OutputMode(mode))
g.stop = make(chan struct{}, 0)
g.tbEvents = make(chan termbox.Event, 20)
g.userEvents = make(chan userEvent, 20)
@@ -107,6 +113,9 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) {
// Close finalizes the library. It should be called after a successful
// initialization and when gocui is not needed anymore.
func (g *Gui) Close() {
go func() {
g.stop <- struct{}{}
}()
termbox.Close()
}
@@ -163,7 +172,7 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int, overlaps byte) (*View, er
v.SelBgColor, v.SelFgColor = g.SelBgColor, g.SelFgColor
v.Overlaps = overlaps
g.views = append(g.views, v)
return v, ErrUnknownView
return v, errors.Wrap(ErrUnknownView, 0)
}
// SetViewOnTop sets the given view on top of the existing ones.
@@ -175,7 +184,7 @@ func (g *Gui) SetViewOnTop(name string) (*View, error) {
return v, nil
}
}
return nil, ErrUnknownView
return nil, errors.Wrap(ErrUnknownView, 0)
}
// SetViewOnBottom sets the given view on bottom of the existing ones.
@@ -187,7 +196,7 @@ func (g *Gui) SetViewOnBottom(name string) (*View, error) {
return v, nil
}
}
return nil, ErrUnknownView
return nil, errors.Wrap(ErrUnknownView, 0)
}
// Views returns all the views in the GUI.
@@ -203,7 +212,7 @@ func (g *Gui) View(name string) (*View, error) {
return v, nil
}
}
return nil, ErrUnknownView
return nil, errors.Wrap(ErrUnknownView, 0)
}
// ViewByPosition returns a pointer to a view matching the given position, or
@@ -216,7 +225,7 @@ func (g *Gui) ViewByPosition(x, y int) (*View, error) {
return v, nil
}
}
return nil, ErrUnknownView
return nil, errors.Wrap(ErrUnknownView, 0)
}
// ViewPosition returns the coordinates of the view with the given name, or
@@ -227,7 +236,7 @@ func (g *Gui) ViewPosition(name string) (x0, y0, x1, y1 int, err error) {
return v.x0, v.y0, v.x1, v.y1, nil
}
}
return 0, 0, 0, 0, ErrUnknownView
return 0, 0, 0, 0, errors.Wrap(ErrUnknownView, 0)
}
// DeleteView deletes a view by name.
@@ -238,7 +247,7 @@ func (g *Gui) DeleteView(name string) error {
return nil
}
}
return ErrUnknownView
return errors.Wrap(ErrUnknownView, 0)
}
// SetCurrentView gives the focus to a given view.
@@ -249,7 +258,7 @@ func (g *Gui) SetCurrentView(name string) (*View, error) {
return v, nil
}
}
return nil, ErrUnknownView
return nil, errors.Wrap(ErrUnknownView, 0)
}
// CurrentView returns the currently focused view, or nil if no view
@@ -364,12 +373,19 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
// MainLoop runs the main loop until an error is returned. A successful
// finish should return ErrQuit.
func (g *Gui) MainLoop() error {
g.loaderTick()
if err := g.flush(); err != nil {
return err
}
go func() {
for {
g.tbEvents <- termbox.PollEvent()
select {
case <-g.stop:
return
default:
g.tbEvents <- termbox.PollEvent()
}
}
}()
@@ -705,3 +721,16 @@ func (g *Gui) execKeybinding(v *View, kb *keybinding) (bool, error) {
}
return true, nil
}
func (g *Gui) loaderTick() {
go func() {
for range time.Tick(time.Millisecond) {
for _, view := range g.Views() {
if view.HasLoader {
g.userEvents <- userEvent{func(g *Gui) error { return nil }}
break
}
}
}
}()
}

View File

@@ -6,9 +6,11 @@ package gocui
import (
"bytes"
"errors"
"io"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/termbox-go"
"github.com/mattn/go-runewidth"
@@ -86,6 +88,9 @@ type View struct {
// Overlaps describes which edges are overlapping with another view's edges
Overlaps byte
// If HasLoader is true, the message will be appended with a spinning loader animation
HasLoader bool
}
type viewLine struct {
@@ -321,7 +326,11 @@ func (v *View) draw() error {
}
if v.tainted {
v.viewLines = nil
for i, line := range v.lines {
lines := v.lines
if v.HasLoader {
lines = v.loaderLines()
}
for i, line := range lines {
wrap := 0
if v.Wrap {
wrap = maxX
@@ -333,7 +342,9 @@ func (v *View) draw() error {
v.viewLines = append(v.viewLines, vline)
}
}
v.tainted = false
if !v.HasLoader {
v.tainted = false
}
}
if v.Autoscroll && len(v.viewLines) > maxY {
@@ -563,3 +574,32 @@ func linesToString(lines [][]cell) string {
return strings.Join(str, "\n")
}
func (v *View) loaderLines() [][]cell {
duplicate := make([][]cell, len(v.lines))
for i := range v.lines {
if i < len(v.lines)-1 {
duplicate[i] = make([]cell, len(v.lines[i]))
copy(duplicate[i], v.lines[i])
} else {
duplicate[i] = make([]cell, len(v.lines[i])+2)
copy(duplicate[i], v.lines[i])
duplicate[i][len(duplicate[i])-2] = cell{chr: ' '}
duplicate[i][len(duplicate[i])-1] = Loader()
}
}
return duplicate
}
func Loader() cell {
characters := "|/-\\"
now := time.Now()
nanos := now.UnixNano()
index := nanos / 50000000 % int64(len(characters))
str := characters[index : index+1]
chr := []rune(str)[0]
return cell{
chr: chr,
}
}