diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05a5a0f7309..3494e6a4046 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,9 +68,8 @@ jobs: restore-keys: | ${{runner.os}}-go- - name: Test code - # LONG_WAIT_BEFORE_FAIL means that for a given test assertion, we'll wait longer before failing run: | - LONG_WAIT_BEFORE_FAIL=true go test pkg/integration/clients/*.go + go test pkg/integration/clients/*.go build: runs-on: ubuntu-latest env: diff --git a/docs/Integration_Tests.md b/docs/Integration_Tests.md deleted file mode 100644 index fab7bb9842c..00000000000 --- a/docs/Integration_Tests.md +++ /dev/null @@ -1 +0,0 @@ -see new docs [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) diff --git a/docs/README.md b/docs/README.md index c0c8191d208..acce8338c7d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,3 +6,4 @@ * [Keybindings](./keybindings) * [Undo/Redo](./Undoing.md) * [Searching/Filtering](./Searching.md) +* [Dev docs](./dev) diff --git a/docs/dev/Busy.md b/docs/dev/Busy.md new file mode 100644 index 00000000000..309f2d25d40 --- /dev/null +++ b/docs/dev/Busy.md @@ -0,0 +1,78 @@ +# Knowing when Lazygit is busy/idle + +## The use-case + +This topic deserves its own doc because there there are a few touch points for it. We have a use-case for knowing when Lazygit is idle or busy because integration tests follow the following process: +1) press a key +2) wait until Lazygit is idle +3) run assertion / press another key +4) repeat + +In the past the process was: +1) press a key +2) run assertion +3) if assertion fails, wait a bit and retry +4) repeat + +The old process was problematic because an assertion may give a false positive due to the contents of some view not yet having changed since the last key was pressed. + +## The solution + +First, it's important to distinguish three different types of goroutines: +* The UI goroutine, of which there is only one, which infinitely processes a queue of events +* Worker goroutines, which do some work and then typically enqueue an event in the UI goroutine to display the results +* Background goroutines, which periodically spawn worker goroutines (e.g. doing a git fetch every minute) + +The point of distinguishing worker goroutines from background goroutines is that when any worker goroutine is running, we consider Lazygit to be 'busy', whereas this is not the case with background goroutines. It would be pointless to have background goroutines be considered 'busy' because then Lazygit would be considered busy for the entire duration of the program! + +In gocui, the underlying package we use for managing the UI and events, we keep track of how many busy goroutines there are using the `Task` type. A task represents some work being done by lazygit. The gocui Gui struct holds a map of tasks and allows creating a new task (which adds it to the map), pausing/continuing a task, and marking a task as done (which removes it from the map). Lazygit is considered to be busy so long as there is at least one busy task in the map; otherwise it's considered idle. When Lazygit goes from busy to idle, it notifies the integration test. + +It's important that we play by the rules below to ensure that after the user does anything, all the processing that follows happens in a contiguous block of busy-ness with no gaps. + +### Spawning a worker goroutine + +Here's the basic implementation of `OnWorker` (using the same flow as `WaitGroup`s): + +```go +func (g *Gui) OnWorker(f func(*Task)) { + task := g.NewTask() + go func() { + f(task) + task.Done() + }() +} +``` + +The crucial thing here is that we create the task _before_ spawning the goroutine, because it means that we'll have at least one busy task in the map until the completion of the goroutine. If we created the task within the goroutine, the current function could exit and Lazygit would be considered idle before the goroutine starts, leading to our integration test prematurely progressing. + +You typically invoke this with `self.c.OnWorker(f)`. Note that the callback function receives the task. This allows the callback to pause/continue the task (see below). + +### Spawning a background goroutine + +Spawning a background goroutine is as simple as: + +```go +go utils.Safe(f) +``` + +Where `utils.Safe` is a helper function that ensures we clean up the gui if the goroutine panics. + +### Programmatically enqueing a UI event + +This is invoked with `self.c.OnUIThread(f)`. Internally, it creates a task before enqueuing the function as an event (including the task in the event struct) and once that event is processed by the event queue (and any other pending events are processed) the task is removed from the map by calling `task.Done()`. + +### Pressing a key + +If the user presses a key, an event will be enqueued automatically and a task will be created before (and `Done`'d after) the event is processed. + +## Special cases + +There are a couple of special cases where we manually pause/continue the task directly in the client code. These are subject to change but for the sake of completeness: + +### Writing to the main view(s) + +If the user focuses a file in the files panel, we run a `git diff` command for that file and write the output to the main view. But we only read enough of the command's output to fill the view's viewport: further loading only happens if the user scrolls. Given that we have a background goroutine for running the command and writing more output upon scrolling, we create our own task and call `Done` on it as soon as the viewport is filled. + +### Requesting credentials from a git command + +Some git commands (e.g. git push) may request credentials. This is the same deal as above; we use a worker goroutine and manually pause continue its task as we go from waiting on the git command to waiting on user input. This requires passing the task through to the `Push` method so that it can be paused/continued. diff --git a/docs/dev/Integration_Tests.md b/docs/dev/Integration_Tests.md new file mode 100644 index 00000000000..df10c3f8fba --- /dev/null +++ b/docs/dev/Integration_Tests.md @@ -0,0 +1 @@ +see new docs [here](../../pkg/integration/README.md) diff --git a/docs/dev/README.md b/docs/dev/README.md new file mode 100644 index 00000000000..9b66032de17 --- /dev/null +++ b/docs/dev/README.md @@ -0,0 +1,4 @@ +# Dev Documentation Overview + +* [Busy/Idle tracking](./Busy.md). +* [Integration Tests](../../pkg/integration/README.md) diff --git a/go.mod b/go.mod index 07ec61e96ea..c074100a609 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/integrii/flaggy v1.4.0 github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d - github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce + github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e @@ -67,8 +67,8 @@ require ( golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/term v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/term v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 19d73cdd7dc..76053ea2d15 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk= github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE= github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o= -github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce h1:Xgm21B1an/outcRxnkDfMT6wKb6SKBR05jXOyfPA8WQ= -github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s= +github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b h1:8FmmdaYHes1m3oNyNdS+VIgkgkFpNZAWuwTnvp0tG14= +github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY= @@ -207,21 +207,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/pkg/commands/git_cmd_obj_runner.go b/pkg/commands/git_cmd_obj_runner.go index 96cef3c610e..163b8514585 100644 --- a/pkg/commands/git_cmd_obj_runner.go +++ b/pkg/commands/git_cmd_obj_runner.go @@ -1,12 +1,20 @@ package commands import ( + "strings" + "time" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/sirupsen/logrus" ) // here we're wrapping the default command runner in some git-specific stuff e.g. retry logic if we get an error due to the presence of .git/index.lock +const ( + WaitTime = 50 * time.Millisecond + RetryCount = 5 +) + type gitCmdObjRunner struct { log *logrus.Entry innerRunner oscommands.ICmdObjRunner @@ -18,13 +26,44 @@ func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error { } func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) { - return self.innerRunner.RunWithOutput(cmdObj) + var output string + var err error + for i := 0; i < RetryCount; i++ { + newCmdObj := cmdObj.Clone() + output, err = self.innerRunner.RunWithOutput(newCmdObj) + + if err == nil || !strings.Contains(output, ".git/index.lock") { + return output, err + } + + // if we have an error based on the index lock, we should wait a bit and then retry + self.log.Warn("index.lock prevented command from running. Retrying command after a small wait") + time.Sleep(WaitTime) + } + + return output, err } func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string, string, error) { - return self.innerRunner.RunWithOutputs(cmdObj) + var stdout, stderr string + var err error + for i := 0; i < RetryCount; i++ { + newCmdObj := cmdObj.Clone() + stdout, stderr, err = self.innerRunner.RunWithOutputs(newCmdObj) + + if err == nil || !strings.Contains(stdout+stderr, ".git/index.lock") { + return stdout, stderr, err + } + + // if we have an error based on the index lock, we should wait a bit and then retry + self.log.Warn("index.lock prevented command from running. Retrying command after a small wait") + time.Sleep(WaitTime) + } + + return stdout, stderr, err } +// Retry logic not implemented here, but these commands typically don't need to obtain a lock. func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error { return self.innerRunner.RunAndProcessLines(cmdObj, onLine) } diff --git a/pkg/commands/git_commands/remote.go b/pkg/commands/git_commands/remote.go index b594db28cd7..b9f20fb3a07 100644 --- a/pkg/commands/git_commands/remote.go +++ b/pkg/commands/git_commands/remote.go @@ -2,6 +2,8 @@ package git_commands import ( "fmt" + + "github.com/jesseduffield/gocui" ) type RemoteCommands struct { @@ -46,12 +48,12 @@ func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string return self.cmd.New(cmdArgs).Run() } -func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error { +func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName string, branchName string) error { cmdArgs := NewGitCmd("push"). Arg(remoteName, "--delete", branchName). ToArgv() - return self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run() + return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() } // CheckRemoteBranchExists Returns remote branch diff --git a/pkg/commands/git_commands/sync.go b/pkg/commands/git_commands/sync.go index dc0a0c68c2b..d67c6aa793a 100644 --- a/pkg/commands/git_commands/sync.go +++ b/pkg/commands/git_commands/sync.go @@ -2,6 +2,7 @@ package git_commands import ( "github.com/go-errors/errors" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" ) @@ -23,7 +24,7 @@ type PushOpts struct { SetUpstream bool } -func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) { +func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (oscommands.ICmdObj, error) { if opts.UpstreamBranch != "" && opts.UpstreamRemote == "" { return nil, errors.New(self.Tr.MustSpecifyOriginError) } @@ -35,12 +36,12 @@ func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) ArgIf(opts.UpstreamBranch != "", opts.UpstreamBranch). ToArgv() - cmdObj := self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex) + cmdObj := self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex) return cmdObj, nil } -func (self *SyncCommands) Push(opts PushOpts) error { - cmdObj, err := self.PushCmdObj(opts) +func (self *SyncCommands) Push(task gocui.Task, opts PushOpts) error { + cmdObj, err := self.PushCmdObj(task, opts) if err != nil { return err } @@ -48,28 +49,33 @@ func (self *SyncCommands) Push(opts PushOpts) error { return cmdObj.Run() } -type FetchOptions struct { - Background bool +func (self *SyncCommands) FetchCmdObj(task gocui.Task) oscommands.ICmdObj { + cmdArgs := NewGitCmd("fetch"). + ArgIf(self.UserConfig.Git.FetchAll, "--all"). + ToArgv() + + cmdObj := self.cmd.New(cmdArgs) + cmdObj.PromptOnCredentialRequest(task) + return cmdObj } -// Fetch fetch git repo -func (self *SyncCommands) FetchCmdObj(opts FetchOptions) oscommands.ICmdObj { +func (self *SyncCommands) Fetch(task gocui.Task) error { + return self.FetchCmdObj(task).Run() +} + +func (self *SyncCommands) FetchBackgroundCmdObj() oscommands.ICmdObj { cmdArgs := NewGitCmd("fetch"). ArgIf(self.UserConfig.Git.FetchAll, "--all"). ToArgv() cmdObj := self.cmd.New(cmdArgs) - if opts.Background { - cmdObj.DontLog().FailOnCredentialRequest() - } else { - cmdObj.PromptOnCredentialRequest() - } - return cmdObj.WithMutex(self.syncMutex) + cmdObj.DontLog().FailOnCredentialRequest() + cmdObj.WithMutex(self.syncMutex) + return cmdObj } -func (self *SyncCommands) Fetch(opts FetchOptions) error { - cmdObj := self.FetchCmdObj(opts) - return cmdObj.Run() +func (self *SyncCommands) FetchBackground() error { + return self.FetchBackgroundCmdObj().Run() } type PullOptions struct { @@ -78,7 +84,7 @@ type PullOptions struct { FastForwardOnly bool } -func (self *SyncCommands) Pull(opts PullOptions) error { +func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error { cmdArgs := NewGitCmd("pull"). Arg("--no-edit"). ArgIf(opts.FastForwardOnly, "--ff-only"). @@ -88,22 +94,22 @@ func (self *SyncCommands) Pull(opts PullOptions) error { // setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user // has 'pull.rebase = interactive' configured. - return self.cmd.New(cmdArgs).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().WithMutex(self.syncMutex).Run() + return self.cmd.New(cmdArgs).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() } -func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error { +func (self *SyncCommands) FastForward(task gocui.Task, branchName string, remoteName string, remoteBranchName string) error { cmdArgs := NewGitCmd("fetch"). Arg(remoteName). Arg(remoteBranchName + ":" + branchName). ToArgv() - return self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run() + return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() } -func (self *SyncCommands) FetchRemote(remoteName string) error { +func (self *SyncCommands) FetchRemote(task gocui.Task, remoteName string) error { cmdArgs := NewGitCmd("fetch"). Arg(remoteName). ToArgv() - return self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run() + return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() } diff --git a/pkg/commands/git_commands/sync_test.go b/pkg/commands/git_commands/sync_test.go index f5eb0d40390..93e6de1b160 100644 --- a/pkg/commands/git_commands/sync_test.go +++ b/pkg/commands/git_commands/sync_test.go @@ -3,6 +3,7 @@ package git_commands import ( "testing" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/stretchr/testify/assert" ) @@ -88,7 +89,8 @@ func TestSyncPush(t *testing.T) { s := s t.Run(s.testName, func(t *testing.T) { instance := buildSyncCommands(commonDeps{}) - s.test(instance.PushCmdObj(s.opts)) + task := gocui.NewFakeTask() + s.test(instance.PushCmdObj(task, s.opts)) }) } } @@ -96,7 +98,6 @@ func TestSyncPush(t *testing.T) { func TestSyncFetch(t *testing.T) { type scenario struct { testName string - opts FetchOptions fetchAllConfig bool test func(oscommands.ICmdObj) } @@ -104,7 +105,6 @@ func TestSyncFetch(t *testing.T) { scenarios := []scenario{ { testName: "Fetch in foreground (all=false)", - opts: FetchOptions{Background: false}, fetchAllConfig: false, test: func(cmdObj oscommands.ICmdObj) { assert.True(t, cmdObj.ShouldLog()) @@ -114,7 +114,6 @@ func TestSyncFetch(t *testing.T) { }, { testName: "Fetch in foreground (all=true)", - opts: FetchOptions{Background: false}, fetchAllConfig: true, test: func(cmdObj oscommands.ICmdObj) { assert.True(t, cmdObj.ShouldLog()) @@ -122,9 +121,29 @@ func TestSyncFetch(t *testing.T) { assert.Equal(t, cmdObj.Args(), []string{"git", "fetch", "--all"}) }, }, + } + + for _, s := range scenarios { + s := s + t.Run(s.testName, func(t *testing.T) { + instance := buildSyncCommands(commonDeps{}) + instance.UserConfig.Git.FetchAll = s.fetchAllConfig + task := gocui.NewFakeTask() + s.test(instance.FetchCmdObj(task)) + }) + } +} + +func TestSyncFetchBackground(t *testing.T) { + type scenario struct { + testName string + fetchAllConfig bool + test func(oscommands.ICmdObj) + } + + scenarios := []scenario{ { testName: "Fetch in background (all=false)", - opts: FetchOptions{Background: true}, fetchAllConfig: false, test: func(cmdObj oscommands.ICmdObj) { assert.False(t, cmdObj.ShouldLog()) @@ -134,7 +153,6 @@ func TestSyncFetch(t *testing.T) { }, { testName: "Fetch in background (all=true)", - opts: FetchOptions{Background: true}, fetchAllConfig: true, test: func(cmdObj oscommands.ICmdObj) { assert.False(t, cmdObj.ShouldLog()) @@ -149,7 +167,7 @@ func TestSyncFetch(t *testing.T) { t.Run(s.testName, func(t *testing.T) { instance := buildSyncCommands(commonDeps{}) instance.UserConfig.Git.FetchAll = s.fetchAllConfig - s.test(instance.FetchCmdObj(s.opts)) + s.test(instance.FetchBackgroundCmdObj()) }) } } diff --git a/pkg/commands/git_commands/tag.go b/pkg/commands/git_commands/tag.go index f399a578ad1..fb87db3b907 100644 --- a/pkg/commands/git_commands/tag.go +++ b/pkg/commands/git_commands/tag.go @@ -1,5 +1,7 @@ package git_commands +import "github.com/jesseduffield/gocui" + type TagCommands struct { *GitCommon } @@ -34,9 +36,9 @@ func (self *TagCommands) Delete(tagName string) error { return self.cmd.New(cmdArgs).Run() } -func (self *TagCommands) Push(remoteName string, tagName string) error { +func (self *TagCommands) Push(task gocui.Task, remoteName string, tagName string) error { cmdArgs := NewGitCmd("push").Arg(remoteName, "tag", tagName). ToArgv() - return self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run() + return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() } diff --git a/pkg/commands/git_config/cached_git_config.go b/pkg/commands/git_config/cached_git_config.go index fe3bc1eca49..da18d086663 100644 --- a/pkg/commands/git_config/cached_git_config.go +++ b/pkg/commands/git_config/cached_git_config.go @@ -3,6 +3,7 @@ package git_config import ( "os/exec" "strings" + "sync" "github.com/sirupsen/logrus" ) @@ -20,6 +21,7 @@ type CachedGitConfig struct { cache map[string]string runGitConfigCmd func(*exec.Cmd) (string, error) log *logrus.Entry + mutex sync.Mutex } func NewStdCachedGitConfig(log *logrus.Entry) *CachedGitConfig { @@ -31,10 +33,14 @@ func NewCachedGitConfig(runGitConfigCmd func(*exec.Cmd) (string, error), log *lo cache: make(map[string]string), runGitConfigCmd: runGitConfigCmd, log: log, + mutex: sync.Mutex{}, } } func (self *CachedGitConfig) Get(key string) string { + self.mutex.Lock() + defer self.mutex.Unlock() + if value, ok := self.cache[key]; ok { self.log.Debugf("using cache for key " + key) return value @@ -46,6 +52,9 @@ func (self *CachedGitConfig) Get(key string) string { } func (self *CachedGitConfig) GetGeneral(args string) string { + self.mutex.Lock() + defer self.mutex.Unlock() + if value, ok := self.cache[args]; ok { self.log.Debugf("using cache for args " + args) return value diff --git a/pkg/commands/oscommands/cmd_obj.go b/pkg/commands/oscommands/cmd_obj.go index b1223ea0056..520a76a1bda 100644 --- a/pkg/commands/oscommands/cmd_obj.go +++ b/pkg/commands/oscommands/cmd_obj.go @@ -4,6 +4,7 @@ import ( "os/exec" "strings" + "github.com/jesseduffield/gocui" "github.com/samber/lo" "github.com/sasha-s/go-deadlock" ) @@ -56,13 +57,16 @@ type ICmdObj interface { // returns true if IgnoreEmptyError() was called ShouldIgnoreEmptyError() bool - PromptOnCredentialRequest() ICmdObj + PromptOnCredentialRequest(task gocui.Task) ICmdObj FailOnCredentialRequest() ICmdObj WithMutex(mutex *deadlock.Mutex) ICmdObj Mutex() *deadlock.Mutex GetCredentialStrategy() CredentialStrategy + GetTask() gocui.Task + + Clone() ICmdObj } type CmdObj struct { @@ -85,6 +89,7 @@ type CmdObj struct { // if set to true, it means we might be asked to enter a username/password by this command. credentialStrategy CredentialStrategy + task gocui.Task // can be set so that we don't run certain commands simultaneously mutex *deadlock.Mutex @@ -192,8 +197,9 @@ func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) e return self.runner.RunAndProcessLines(self, onLine) } -func (self *CmdObj) PromptOnCredentialRequest() ICmdObj { +func (self *CmdObj) PromptOnCredentialRequest(task gocui.Task) ICmdObj { self.credentialStrategy = PROMPT + self.task = task return self } @@ -207,3 +213,21 @@ func (self *CmdObj) FailOnCredentialRequest() ICmdObj { func (self *CmdObj) GetCredentialStrategy() CredentialStrategy { return self.credentialStrategy } + +func (self *CmdObj) GetTask() gocui.Task { + return self.task +} + +func (self *CmdObj) Clone() ICmdObj { + clone := &CmdObj{} + *clone = *self + clone.cmd = cloneCmd(self.cmd) + return clone +} + +func cloneCmd(cmd *exec.Cmd) *exec.Cmd { + clone := &exec.Cmd{} + *clone = *cmd + + return clone +} diff --git a/pkg/commands/oscommands/cmd_obj_runner.go b/pkg/commands/oscommands/cmd_obj_runner.go index 3df1e291634..3456bcd9e23 100644 --- a/pkg/commands/oscommands/cmd_obj_runner.go +++ b/pkg/commands/oscommands/cmd_obj_runner.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/go-errors/errors" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" ) @@ -19,15 +20,6 @@ type ICmdObjRunner interface { RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error } -type CredentialType int - -const ( - Password CredentialType = iota - Username - Passphrase - PIN -) - type cmdObjRunner struct { log *logrus.Entry guiIO *guiIO @@ -182,26 +174,6 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st return nil } -// Whenever we're asked for a password we just enter a newline, which will -// eventually cause the command to fail. -var failPromptFn = func(CredentialType) string { return "\n" } - -func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error { - var promptFn func(CredentialType) string - - switch cmdObj.GetCredentialStrategy() { - case PROMPT: - promptFn = self.guiIO.promptForCredentialFn - case FAIL: - promptFn = failPromptFn - case NONE: - // we should never land here - return errors.New("runWithCredentialHandling called but cmdObj does not have a credential strategy") - } - - return self.runAndDetectCredentialRequest(cmdObj, promptFn) -} - func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) { self.guiIO.logCommandFn(cmdObj.ToString(), true) } @@ -233,25 +205,6 @@ func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error { }) } -// runAndDetectCredentialRequest detect a username / password / passphrase question in a command -// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase -// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back -func (self *cmdObjRunner) runAndDetectCredentialRequest( - cmdObj ICmdObj, - promptUserForCredential func(CredentialType) string, -) error { - // setting the output to english so we can parse it for a username/password request - cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8") - - return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) { - tr := io.TeeReader(handler.stdoutPipe, cmdWriter) - - go utils.Safe(func() { - self.processOutput(tr, handler.stdinPipe, promptUserForCredential) - }) - }) -} - func (self *cmdObjRunner) runAndStreamAux( cmdObj ICmdObj, onRun func(*cmdHandler, io.Writer), @@ -296,13 +249,79 @@ func (self *cmdObjRunner) runAndStreamAux( if cmdObj.ShouldIgnoreEmptyError() { return nil } - return errors.New(stdout.String()) + stdoutStr := stdout.String() + if stdoutStr != "" { + return errors.New(stdoutStr) + } + return errors.New("Command exited with non-zero exit code, but no output") } return nil } -func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, promptUserForCredential func(CredentialType) string) { +type CredentialType int + +const ( + Password CredentialType = iota + Username + Passphrase + PIN +) + +// Whenever we're asked for a password we just enter a newline, which will +// eventually cause the command to fail. +var failPromptFn = func(CredentialType) <-chan string { + ch := make(chan string) + go func() { + ch <- "\n" + }() + return ch +} + +func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error { + promptFn, err := self.getCredentialPromptFn(cmdObj) + if err != nil { + return err + } + + return self.runAndDetectCredentialRequest(cmdObj, promptFn) +} + +func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(CredentialType) <-chan string, error) { + switch cmdObj.GetCredentialStrategy() { + case PROMPT: + return self.guiIO.promptForCredentialFn, nil + case FAIL: + return failPromptFn, nil + default: + // we should never land here + return nil, errors.New("runWithCredentialHandling called but cmdObj does not have a credential strategy") + } +} + +// runAndDetectCredentialRequest detect a username / password / passphrase question in a command +// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase +// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back +func (self *cmdObjRunner) runAndDetectCredentialRequest( + cmdObj ICmdObj, + promptUserForCredential func(CredentialType) <-chan string, +) error { + // setting the output to english so we can parse it for a username/password request + cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8") + + return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) { + tr := io.TeeReader(handler.stdoutPipe, cmdWriter) + + self.processOutput(tr, handler.stdinPipe, promptUserForCredential, cmdObj.GetTask()) + }) +} + +func (self *cmdObjRunner) processOutput( + reader io.Reader, + writer io.Writer, + promptUserForCredential func(CredentialType) <-chan string, + task gocui.Task, +) { checkForCredentialRequest := self.getCheckForCredentialRequestFunc() scanner := bufio.NewScanner(reader) @@ -311,7 +330,10 @@ func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, prom newBytes := scanner.Bytes() askFor, ok := checkForCredentialRequest(newBytes) if ok { - toInput := promptUserForCredential(askFor) + responseChan := promptUserForCredential(askFor) + task.Pause() + toInput := <-responseChan + task.Continue() // If the return data is empty we don't write anything to stdin if toInput != "" { _, _ = writer.Write([]byte(toInput)) diff --git a/pkg/commands/oscommands/cmd_obj_runner_test.go b/pkg/commands/oscommands/cmd_obj_runner_test.go index ab26b982716..31966cec1c9 100644 --- a/pkg/commands/oscommands/cmd_obj_runner_test.go +++ b/pkg/commands/oscommands/cmd_obj_runner_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -15,6 +16,18 @@ func getRunner() *cmdObjRunner { } } +func toChanFn(f func(ct CredentialType) string) func(CredentialType) <-chan string { + return func(ct CredentialType) <-chan string { + ch := make(chan string) + + go func() { + ch <- f(ct) + }() + + return ch + } +} + func TestProcessOutput(t *testing.T) { defaultPromptUserForCredential := func(ct CredentialType) string { switch ct { @@ -99,7 +112,8 @@ func TestProcessOutput(t *testing.T) { reader := strings.NewReader(scenario.output) writer := &strings.Builder{} - runner.processOutput(reader, writer, scenario.promptUserForCredential) + task := gocui.NewFakeTask() + runner.processOutput(reader, writer, toChanFn(scenario.promptUserForCredential), task) if writer.String() != scenario.expectedToWrite { t.Errorf("expected to write '%s' but got '%s'", scenario.expectedToWrite, writer.String()) diff --git a/pkg/commands/oscommands/gui_io.go b/pkg/commands/oscommands/gui_io.go index 10a8b267815..6a61983109d 100644 --- a/pkg/commands/oscommands/gui_io.go +++ b/pkg/commands/oscommands/gui_io.go @@ -26,10 +26,15 @@ type guiIO struct { // this allows us to request info from the user like username/password, in the event // that a command requests it. // the 'credential' arg is something like 'username' or 'password' - promptForCredentialFn func(credential CredentialType) string + promptForCredentialFn func(credential CredentialType) <-chan string } -func NewGuiIO(log *logrus.Entry, logCommandFn func(string, bool), newCmdWriterFn func() io.Writer, promptForCredentialFn func(CredentialType) string) *guiIO { +func NewGuiIO( + log *logrus.Entry, + logCommandFn func(string, bool), + newCmdWriterFn func() io.Writer, + promptForCredentialFn func(CredentialType) <-chan string, +) *guiIO { return &guiIO{ log: log, logCommandFn: logCommandFn, diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 14ee70187d2..342e3127ecb 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -4,7 +4,7 @@ import ( "strings" "time" - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -15,11 +15,11 @@ type BackgroundRoutineMgr struct { // if we've suspended the gui (e.g. because we've switched to a subprocess) // we typically want to pause some things that are running like background // file refreshes - pauseBackgroundThreads bool + pauseBackgroundRefreshes bool } -func (self *BackgroundRoutineMgr) PauseBackgroundThreads(pause bool) { - self.pauseBackgroundThreads = pause +func (self *BackgroundRoutineMgr) PauseBackgroundRefreshes(pause bool) { + self.pauseBackgroundRefreshes = pause } func (self *BackgroundRoutineMgr) startBackgroundRoutines() { @@ -39,9 +39,7 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() { if userConfig.Git.AutoRefresh { refreshInterval := userConfig.Refresher.RefreshInterval if refreshInterval > 0 { - self.goEvery(time.Second*time.Duration(refreshInterval), self.gui.stopChan, func() error { - return self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) - }) + go utils.Safe(func() { self.startBackgroundFilesRefresh(refreshInterval) }) } else { self.gui.c.Log.Errorf( "Value of config option 'refresher.refreshInterval' (%d) is invalid, disabling auto-refresh", @@ -52,6 +50,7 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() { func (self *BackgroundRoutineMgr) startBackgroundFetch() { self.gui.waitForIntro.Wait() + isNew := self.gui.IsNewRepo userConfig := self.gui.UserConfig if !isNew { @@ -69,17 +68,31 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() { } } +func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh(refreshInterval int) { + self.gui.waitForIntro.Wait() + + self.goEvery(time.Second*time.Duration(refreshInterval), self.gui.stopChan, func() error { + return self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) + }) +} + func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func() error) { + done := make(chan struct{}) go utils.Safe(func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: - if self.pauseBackgroundThreads { + if self.pauseBackgroundRefreshes { continue } - _ = function() + self.gui.c.OnWorker(func(gocui.Task) { + _ = function() + done <- struct{}{} + }) + // waiting so that we don't bunch up refreshes if the refresh takes longer than the interval + <-done case <-stop: return } @@ -88,7 +101,7 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru } func (self *BackgroundRoutineMgr) backgroundFetch() (err error) { - err = self.gui.git.Sync.Fetch(git_commands.FetchOptions{Background: true}) + err = self.gui.git.Sync.FetchBackground() _ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC}) diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index 58b2205a423..d8b65064231 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -30,7 +30,7 @@ func NewSuggestionsContext( c *ContextCommon, ) *SuggestionsContext { state := &SuggestionsContextState{ - AsyncHandler: tasks.NewAsyncHandler(), + AsyncHandler: tasks.NewAsyncHandler(c.OnWorker), } getModel := func() []*types.Suggestion { return state.Suggestions diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 972ea609047..c55a3904118 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" @@ -363,11 +364,12 @@ func (self *BranchesController) fastForward(branch *models.Branch) error { }, ) - return self.c.WithLoaderPanel(message, func() error { + return self.c.WithLoaderPanel(message, func(task gocui.Task) error { if branch == self.c.Helpers().Refs.GetCheckedOutRef() { self.c.LogAction(action) err := self.c.Git().Sync.Pull( + task, git_commands.PullOptions{ RemoteName: branch.UpstreamRemote, BranchName: branch.UpstreamBranch, @@ -381,7 +383,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error { return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) } else { self.c.LogAction(action) - err := self.c.Git().Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch) + err := self.c.Git().Sync.FastForward(task, branch.Name, branch.UpstreamRemote, branch.UpstreamBranch) if err != nil { _ = self.c.Error(err) } diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 745186df06d..a1cd6a9ca65 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -177,7 +177,7 @@ func (self *CommitFilesController) discard(node *filetree.CommitFileNode) error Title: self.c.Tr.DiscardFileChangesTitle, Prompt: prompt, HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.DiscardOldFileChange) if err := self.c.Git().Rebase.DiscardOldFileChanges(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), node.GetPath()); err != nil { if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err); err != nil { @@ -205,7 +205,7 @@ func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error { func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode) error { toggle := func() error { - return self.c.WithWaitingStatus(self.c.Tr.UpdatingPatch, func() error { + return self.c.WithWaitingStatus(self.c.Tr.UpdatingPatch, func(gocui.Task) error { if !self.c.Git().Patch.PatchBuilder.Active() { if err := self.startPatchBuilder(); err != nil { return err diff --git a/pkg/gui/controllers/custom_patch_options_menu_action.go b/pkg/gui/controllers/custom_patch_options_menu_action.go index a511348bbf1..55793f8f5a6 100644 --- a/pkg/gui/controllers/custom_patch_options_menu_action.go +++ b/pkg/gui/controllers/custom_patch_options_menu_action.go @@ -3,6 +3,7 @@ package controllers import ( "fmt" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -116,7 +117,7 @@ func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error { return err } - return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error { commitIndex := self.getPatchCommitIndex() self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit) err := self.c.Git().Patch.DeletePatchesFromCommit(self.c.Model().Commits, commitIndex) @@ -133,7 +134,7 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchToSelectedCommit() erro return err } - return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error { commitIndex := self.getPatchCommitIndex() self.c.LogAction(self.c.Tr.Actions.MovePatchToSelectedCommit) err := self.c.Git().Patch.MovePatchToSelectedCommit(self.c.Model().Commits, commitIndex, self.c.Contexts().LocalCommits.GetSelectedLineIdx()) @@ -151,7 +152,7 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error } pull := func(stash bool) error { - return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error { commitIndex := self.getPatchCommitIndex() self.c.LogAction(self.c.Tr.Actions.MovePatchIntoIndex) err := self.c.Git().Patch.MovePatchIntoIndex(self.c.Model().Commits, commitIndex, stash) @@ -181,7 +182,7 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error { return err } - return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error { commitIndex := self.getPatchCommitIndex() self.c.LogAction(self.c.Tr.Actions.MovePatchIntoNewCommit) err := self.c.Git().Patch.PullPatchIntoNewCommit(self.c.Model().Commits, commitIndex) diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 61d91ad69e0..6d4647e01dc 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/filetree" @@ -801,17 +800,17 @@ func (self *FilesController) onClickSecondary(opts gocui.ViewMouseBindingOpts) e } func (self *FilesController) fetch() error { - return self.c.WithLoaderPanel(self.c.Tr.FetchWait, func() error { - if err := self.fetchAux(); err != nil { + return self.c.WithLoaderPanel(self.c.Tr.FetchWait, func(task gocui.Task) error { + if err := self.fetchAux(task); err != nil { _ = self.c.Error(err) } return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) }) } -func (self *FilesController) fetchAux() (err error) { +func (self *FilesController) fetchAux(task gocui.Task) (err error) { self.c.LogAction("Fetch") - err = self.c.Git().Sync.Fetch(git_commands.FetchOptions{}) + err = self.c.Git().Sync.Fetch(task) if err != nil && strings.Contains(err.Error(), "exit status 128") { _ = self.c.ErrorMsg(self.c.Tr.PassUnameWrong) diff --git a/pkg/gui/controllers/files_remove_controller.go b/pkg/gui/controllers/files_remove_controller.go index f25ae12098e..dd3a3c9c5e7 100644 --- a/pkg/gui/controllers/files_remove_controller.go +++ b/pkg/gui/controllers/files_remove_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/filetree" @@ -145,7 +146,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error { } func (self *FilesRemoveController) ResetSubmodule(submodule *models.SubmoduleConfig) error { - return self.c.WithWaitingStatus(self.c.Tr.ResettingSubmoduleStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.ResettingSubmoduleStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.ResetSubmodule) file := self.c.Helpers().WorkingTree.FileForSubmodule(submodule) diff --git a/pkg/gui/controllers/helpers/app_status_helper.go b/pkg/gui/controllers/helpers/app_status_helper.go index f125ebf7b63..e3b6931ad2b 100644 --- a/pkg/gui/controllers/helpers/app_status_helper.go +++ b/pkg/gui/controllers/helpers/app_status_helper.go @@ -3,8 +3,8 @@ package helpers import ( "time" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/status" - "github.com/jesseduffield/lazygit/pkg/utils" ) type AppStatusHelper struct { @@ -27,12 +27,12 @@ func (self *AppStatusHelper) Toast(message string) { } // withWaitingStatus wraps a function and shows a waiting status while the function is still executing -func (self *AppStatusHelper) WithWaitingStatus(message string, f func() error) { - go utils.Safe(func() { +func (self *AppStatusHelper) WithWaitingStatus(message string, f func(gocui.Task) error) { + self.c.OnWorker(func(task gocui.Task) { self.statusMgr().WithWaitingStatus(message, func() { self.renderAppStatus() - if err := f(); err != nil { + if err := f(task); err != nil { self.c.OnUIThread(func() error { return self.c.Error(err) }) @@ -50,7 +50,7 @@ func (self *AppStatusHelper) GetStatusString() string { } func (self *AppStatusHelper) renderAppStatus() { - go utils.Safe(func() { + self.c.OnWorker(func(_ gocui.Task) { ticker := time.NewTicker(time.Millisecond * 50) defer ticker.Stop() for range ticker.C { diff --git a/pkg/gui/controllers/helpers/cherry_pick_helper.go b/pkg/gui/controllers/helpers/cherry_pick_helper.go index 2c9b7c7c9db..2e8a11f7d1a 100644 --- a/pkg/gui/controllers/helpers/cherry_pick_helper.go +++ b/pkg/gui/controllers/helpers/cherry_pick_helper.go @@ -1,6 +1,7 @@ package helpers import ( + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -75,7 +76,7 @@ func (self *CherryPickHelper) Paste() error { Title: self.c.Tr.CherryPick, Prompt: self.c.Tr.SureCherryPick, HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.CherryPickingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.CherryPickingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.CherryPick) err := self.c.Git().Rebase.CherryPickCommits(self.getData().CherryPickedCommits) return self.rebaseHelper.CheckMergeOrRebase(err) diff --git a/pkg/gui/controllers/helpers/credentials_helper.go b/pkg/gui/controllers/helpers/credentials_helper.go index 0aed34110a5..20fb5905202 100644 --- a/pkg/gui/controllers/helpers/credentials_helper.go +++ b/pkg/gui/controllers/helpers/credentials_helper.go @@ -1,8 +1,6 @@ package helpers import ( - "sync" - "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -20,11 +18,11 @@ func NewCredentialsHelper( } // promptUserForCredential wait for a username, password or passphrase input from the credentials popup -func (self *CredentialsHelper) PromptUserForCredential(passOrUname oscommands.CredentialType) string { - waitGroup := sync.WaitGroup{} - waitGroup.Add(1) - - userInput := "" +// We return a channel rather than returning the string directly so that the calling function knows +// when the prompt has been created (before the user has entered anything) so that it can +// note that we're now waiting on user input and lazygit isn't processing anything. +func (self *CredentialsHelper) PromptUserForCredential(passOrUname oscommands.CredentialType) <-chan string { + ch := make(chan string) self.c.OnUIThread(func() error { title, mask := self.getTitleAndMask(passOrUname) @@ -33,24 +31,19 @@ func (self *CredentialsHelper) PromptUserForCredential(passOrUname oscommands.Cr Title: title, Mask: mask, HandleConfirm: func(input string) error { - userInput = input - - waitGroup.Done() + ch <- input + "\n" return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) }, HandleClose: func() error { - waitGroup.Done() + ch <- "\n" return nil }, }) }) - // wait for username/passwords/passphrase input - waitGroup.Wait() - - return userInput + "\n" + return ch } func (self *CredentialsHelper) getTitleAndMask(passOrUname oscommands.CredentialType) (string, bool) { diff --git a/pkg/gui/controllers/helpers/gpg_helper.go b/pkg/gui/controllers/helpers/gpg_helper.go index 45d67faaf60..df440104b89 100644 --- a/pkg/gui/controllers/helpers/gpg_helper.go +++ b/pkg/gui/controllers/helpers/gpg_helper.go @@ -3,6 +3,7 @@ package helpers import ( "fmt" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -41,7 +42,7 @@ func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus } func (self *GpgHelper) runAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error { - return self.c.WithWaitingStatus(waitingStatus, func() error { + return self.c.WithWaitingStatus(waitingStatus, func(gocui.Task) error { if err := cmdObj.StreamOutput().Run(); err != nil { _ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return self.c.Error( diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index f0827dc41a5..36b960c6920 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/generics/set" "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" @@ -63,8 +64,6 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error { ) } - wg := sync.WaitGroup{} - f := func() { var scopeSet *set.Set[types.RefreshableView] if len(options.Scope) == 0 { @@ -87,15 +86,13 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error { } refresh := func(f func()) { - wg.Add(1) - func() { - if options.Mode == types.ASYNC { - go utils.Safe(f) - } else { + if options.Mode == types.ASYNC { + self.c.OnWorker(func(t gocui.Task) { f() - } - wg.Done() - }() + }) + } else { + f() + } } if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) { @@ -143,8 +140,6 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error { refresh(func() { _ = self.mergeConflictsHelper.RefreshMergeState() }) } - wg.Wait() - self.refreshStatus() if options.Then != nil { @@ -206,7 +201,7 @@ func getModeName(mode types.RefreshMode) string { func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() { switch self.c.State().GetRepoState().GetStartupStage() { case types.INITIAL: - go utils.Safe(func() { + self.c.OnWorker(func(_ gocui.Task) { _ = self.refreshReflogCommits() self.refreshBranches() self.c.State().GetRepoState().SetStartupStage(types.COMPLETE) @@ -350,6 +345,9 @@ func (self *RefreshHelper) refreshStateSubmoduleConfigs() error { // self.refreshStatus is called at the end of this because that's when we can // be sure there is a State.Model.Branches array to pick the current branch from func (self *RefreshHelper) refreshBranches() { + self.c.Mutexes().RefreshingBranchesMutex.Lock() + defer self.c.Mutexes().RefreshingBranchesMutex.Unlock() + reflogCommits := self.c.Model().FilteredReflogCommits if self.c.Modes().Filtering.Active() { // in filter mode we filter our reflog commits to just those containing the path diff --git a/pkg/gui/controllers/helpers/refs_helper.go b/pkg/gui/controllers/helpers/refs_helper.go index 18227a35bec..af3a0875f84 100644 --- a/pkg/gui/controllers/helpers/refs_helper.go +++ b/pkg/gui/controllers/helpers/refs_helper.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/style" @@ -50,7 +51,7 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions self.c.Contexts().LocalCommits.SetLimitCommits(true) } - return self.c.WithWaitingStatus(waitingStatus, func() error { + return self.c.WithWaitingStatus(waitingStatus, func(gocui.Task) error { if err := self.c.Git().Branch.Checkout(ref, cmdOptions); err != nil { // note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go index 70fcf168ad3..78357661701 100644 --- a/pkg/gui/controllers/helpers/suggestions_helper.go +++ b/pkg/gui/controllers/helpers/suggestions_helper.go @@ -5,6 +5,7 @@ import ( "os" "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -100,7 +101,7 @@ func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*ty // Notably, unlike other suggestion functions we're not showing all the options // if nothing has been typed because there'll be too much to display efficiently func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*types.Suggestion { - _ = self.c.WithWaitingStatus(self.c.Tr.LoadingFileSuggestions, func() error { + _ = self.c.WithWaitingStatus(self.c.Tr.LoadingFileSuggestions, func(gocui.Task) error { trie := patricia.NewTrie() // load every non-gitignored file in the repo ignore, err := gitignore.FromGit() diff --git a/pkg/gui/controllers/helpers/update_helper.go b/pkg/gui/controllers/helpers/update_helper.go index ea9be8f1657..36cb6558a0f 100644 --- a/pkg/gui/controllers/helpers/update_helper.go +++ b/pkg/gui/controllers/helpers/update_helper.go @@ -1,6 +1,7 @@ package helpers import ( + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/updates" "github.com/jesseduffield/lazygit/pkg/utils" @@ -37,7 +38,7 @@ func (self *UpdateHelper) CheckForUpdateInBackground() { } func (self *UpdateHelper) CheckForUpdateInForeground() error { - return self.c.WithWaitingStatus(self.c.Tr.CheckingForUpdates, func() error { + return self.c.WithWaitingStatus(self.c.Tr.CheckingForUpdates, func(gocui.Task) error { self.updater.CheckForNewUpdate(func(newVersion string, err error) error { if err != nil { return self.c.Error(err) @@ -53,7 +54,7 @@ func (self *UpdateHelper) CheckForUpdateInForeground() error { } func (self *UpdateHelper) startUpdating(newVersion string) { - _ = self.c.WithWaitingStatus(self.c.Tr.UpdateInProgressWaitingStatus, func() error { + _ = self.c.WithWaitingStatus(self.c.Tr.UpdateInProgressWaitingStatus, func(gocui.Task) error { self.c.State().SetUpdating(true) err := self.updater.Update(newVersion) return self.onUpdateFinish(err) diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 49abe02ff14..11e1bac24a2 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/fsmiamoto/git-todo-parser/todo" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/gui/context" @@ -217,7 +218,7 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error { Title: self.c.Tr.Squash, Prompt: self.c.Tr.SureSquashThisCommit, HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.SquashCommitDown) return self.interactiveRebase(todo.Squash) }) @@ -242,7 +243,7 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error { Title: self.c.Tr.Fixup, Prompt: self.c.Tr.SureFixupThisCommit, HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.FixupCommit) return self.interactiveRebase(todo.Fixup) }) @@ -338,7 +339,7 @@ func (self *LocalCommitsController) drop(commit *models.Commit) error { Title: self.c.Tr.DeleteCommitTitle, Prompt: self.c.Tr.DeleteCommitPrompt, HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.DropCommit) return self.interactiveRebase(todo.Drop) }) @@ -355,7 +356,7 @@ func (self *LocalCommitsController) edit(commit *models.Commit) error { return nil } - return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.EditCommit) err := self.c.Git().Rebase.EditRebase(commit.Sha) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) @@ -460,7 +461,7 @@ func (self *LocalCommitsController) moveDown(commit *models.Commit) error { return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) } - return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.MoveCommitDown) err := self.c.Git().Rebase.MoveCommitDown(self.c.Model().Commits, index) if err == nil { @@ -498,7 +499,7 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error { return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) } - return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.MoveCommitUp) err := self.c.Git().Rebase.MoveCommitUp(self.c.Model().Commits, index) if err == nil { @@ -524,7 +525,7 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error { Title: self.c.Tr.AmendCommitTitle, Prompt: self.c.Tr.AmendCommitPrompt, HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.AmendCommit) err := self.c.Git().Rebase.AmendTo(self.c.Model().Commits, self.context().GetView().SelectedLineIdx()) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) @@ -558,7 +559,7 @@ func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error } func (self *LocalCommitsController) resetAuthor() error { - return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.ResetCommitAuthor) if err := self.c.Git().Rebase.ResetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx()); err != nil { return self.c.Error(err) @@ -573,7 +574,7 @@ func (self *LocalCommitsController) setAuthor() error { Title: self.c.Tr.SetAuthorPromptTitle, FindSuggestionsFunc: self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc(), HandleConfirm: func(value string) error { - return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.SetCommitAuthor) if err := self.c.Git().Rebase.SetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx(), value); err != nil { return self.c.Error(err) @@ -671,7 +672,7 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co Title: self.c.Tr.SquashAboveCommits, Prompt: prompt, HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits) err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) @@ -723,7 +724,7 @@ func (self *LocalCommitsController) handleOpenLogMenu() error { self.context().SetLimitCommits(false) } - return self.c.WithWaitingStatus(self.c.Tr.LoadingCommits, func() error { + return self.c.WithWaitingStatus(self.c.Tr.LoadingCommits, func(gocui.Task) error { return self.c.Refresh( types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS}}, ) @@ -766,7 +767,7 @@ func (self *LocalCommitsController) handleOpenLogMenu() error { onPress := func(value string) func() error { return func() error { self.c.UserConfig.Git.Log.Order = value - return self.c.WithWaitingStatus(self.c.Tr.LoadingCommits, func() error { + return self.c.WithWaitingStatus(self.c.Tr.LoadingCommits, func(gocui.Task) error { return self.c.Refresh( types.RefreshOptions{ Mode: types.SYNC, @@ -816,7 +817,7 @@ func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error { context := self.context() if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { context.SetLimitCommits(false) - go utils.Safe(func() { + self.c.OnWorker(func(_ gocui.Task) { if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}); err != nil { _ = self.c.Error(err) } diff --git a/pkg/gui/controllers/remote_branches_controller.go b/pkg/gui/controllers/remote_branches_controller.go index b26230d9018..529b00a906d 100644 --- a/pkg/gui/controllers/remote_branches_controller.go +++ b/pkg/gui/controllers/remote_branches_controller.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -117,9 +118,9 @@ func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch Title: self.c.Tr.DeleteRemoteBranch, Prompt: message, HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch) - err := self.c.Git().Remote.DeleteRemoteBranch(selectedBranch.RemoteName, selectedBranch.Name) + err := self.c.Git().Remote.DeleteRemoteBranch(task, selectedBranch.RemoteName, selectedBranch.Name) if err != nil { _ = self.c.Error(err) } diff --git a/pkg/gui/controllers/remotes_controller.go b/pkg/gui/controllers/remotes_controller.go index b6d9a963bd1..56dc466c5ef 100644 --- a/pkg/gui/controllers/remotes_controller.go +++ b/pkg/gui/controllers/remotes_controller.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/style" @@ -197,8 +198,8 @@ func (self *RemotesController) edit(remote *models.Remote) error { } func (self *RemotesController) fetch(remote *models.Remote) error { - return self.c.WithWaitingStatus(self.c.Tr.FetchingRemoteStatus, func() error { - err := self.c.Git().Sync.FetchRemote(remote.Name) + return self.c.WithWaitingStatus(self.c.Tr.FetchingRemoteStatus, func(task gocui.Task) error { + err := self.c.Git().Sync.FetchRemote(task, remote.Name) if err != nil { _ = self.c.Error(err) } diff --git a/pkg/gui/controllers/sub_commits_controller.go b/pkg/gui/controllers/sub_commits_controller.go index 485d4982071..0855e668927 100644 --- a/pkg/gui/controllers/sub_commits_controller.go +++ b/pkg/gui/controllers/sub_commits_controller.go @@ -1,9 +1,9 @@ package controllers import ( + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" ) type SubCommitsController struct { @@ -60,7 +60,7 @@ func (self *SubCommitsController) GetOnFocus() func(types.OnFocusOpts) error { context := self.context() if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { context.SetLimitCommits(false) - go utils.Safe(func() { + self.c.OnWorker(func(_ gocui.Task) { if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUB_COMMITS}}); err != nil { _ = self.c.Error(err) } diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index 80ac54cd71d..b2cb006355f 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/style" @@ -130,7 +131,7 @@ func (self *SubmodulesController) add() error { Title: self.c.Tr.NewSubmodulePath, InitialContent: submoduleName, HandleConfirm: func(submodulePath string) error { - return self.c.WithWaitingStatus(self.c.Tr.AddingSubmoduleStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.AddingSubmoduleStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.AddSubmodule) err := self.c.Git().Submodule.Add(submoduleName, submodulePath, submoduleUrl) if err != nil { @@ -152,7 +153,7 @@ func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) err Title: fmt.Sprintf(self.c.Tr.UpdateSubmoduleUrl, submodule.Name), InitialContent: submodule.Url, HandleConfirm: func(newUrl string) error { - return self.c.WithWaitingStatus(self.c.Tr.UpdatingSubmoduleUrlStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.UpdatingSubmoduleUrlStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.UpdateSubmoduleUrl) err := self.c.Git().Submodule.UpdateUrl(submodule.Name, submodule.Path, newUrl) if err != nil { @@ -166,7 +167,7 @@ func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) err } func (self *SubmodulesController) init(submodule *models.SubmoduleConfig) error { - return self.c.WithWaitingStatus(self.c.Tr.InitializingSubmoduleStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.InitializingSubmoduleStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.InitialiseSubmodule) err := self.c.Git().Submodule.Init(submodule.Path) if err != nil { @@ -184,7 +185,7 @@ func (self *SubmodulesController) openBulkActionsMenu() error { { LabelColumns: []string{self.c.Tr.BulkInitSubmodules, style.FgGreen.Sprint(self.c.Git().Submodule.BulkInitCmdObj().ToString())}, OnPress: func() error { - return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func() error { + return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.BulkInitialiseSubmodules) err := self.c.Git().Submodule.BulkInitCmdObj().Run() if err != nil { @@ -199,7 +200,7 @@ func (self *SubmodulesController) openBulkActionsMenu() error { { LabelColumns: []string{self.c.Tr.BulkUpdateSubmodules, style.FgYellow.Sprint(self.c.Git().Submodule.BulkUpdateCmdObj().ToString())}, OnPress: func() error { - return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func() error { + return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.BulkUpdateSubmodules) if err := self.c.Git().Submodule.BulkUpdateCmdObj().Run(); err != nil { return self.c.Error(err) @@ -213,7 +214,7 @@ func (self *SubmodulesController) openBulkActionsMenu() error { { LabelColumns: []string{self.c.Tr.BulkDeinitSubmodules, style.FgRed.Sprint(self.c.Git().Submodule.BulkDeinitCmdObj().ToString())}, OnPress: func() error { - return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func() error { + return self.c.WithWaitingStatus(self.c.Tr.RunningCommand, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.BulkDeinitialiseSubmodules) if err := self.c.Git().Submodule.BulkDeinitCmdObj().Run(); err != nil { return self.c.Error(err) @@ -229,7 +230,7 @@ func (self *SubmodulesController) openBulkActionsMenu() error { } func (self *SubmodulesController) update(submodule *models.SubmoduleConfig) error { - return self.c.WithWaitingStatus(self.c.Tr.UpdatingSubmoduleStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.UpdatingSubmoduleStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.UpdateSubmodule) err := self.c.Git().Submodule.Update(submodule.Path) if err != nil { diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go index 66373cca7e1..9fa2da09c95 100644 --- a/pkg/gui/controllers/sync_controller.go +++ b/pkg/gui/controllers/sync_controller.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -138,15 +139,16 @@ type PullFilesOptions struct { } func (self *SyncController) PullAux(opts PullFilesOptions) error { - return self.c.WithLoaderPanel(self.c.Tr.PullWait, func() error { - return self.pullWithLock(opts) + return self.c.WithLoaderPanel(self.c.Tr.PullWait, func(task gocui.Task) error { + return self.pullWithLock(task, opts) }) } -func (self *SyncController) pullWithLock(opts PullFilesOptions) error { +func (self *SyncController) pullWithLock(task gocui.Task, opts PullFilesOptions) error { self.c.LogAction(opts.Action) err := self.c.Git().Sync.Pull( + task, git_commands.PullOptions{ RemoteName: opts.UpstreamRemote, BranchName: opts.UpstreamBranch, @@ -165,14 +167,16 @@ type pushOpts struct { } func (self *SyncController) pushAux(opts pushOpts) error { - return self.c.WithLoaderPanel(self.c.Tr.PushWait, func() error { + return self.c.WithLoaderPanel(self.c.Tr.PushWait, func(task gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.Push) - err := self.c.Git().Sync.Push(git_commands.PushOpts{ - Force: opts.force, - UpstreamRemote: opts.upstreamRemote, - UpstreamBranch: opts.upstreamBranch, - SetUpstream: opts.setUpstream, - }) + err := self.c.Git().Sync.Push( + task, + git_commands.PushOpts{ + Force: opts.force, + UpstreamRemote: opts.upstreamRemote, + UpstreamBranch: opts.upstreamBranch, + SetUpstream: opts.setUpstream, + }) if err != nil { if !opts.force && strings.Contains(err.Error(), "Updates were rejected") { forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing diff --git a/pkg/gui/controllers/tags_controller.go b/pkg/gui/controllers/tags_controller.go index 79d5f546672..df43c1f6d5e 100644 --- a/pkg/gui/controllers/tags_controller.go +++ b/pkg/gui/controllers/tags_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -121,9 +122,9 @@ func (self *TagsController) push(tag *models.Tag) error { InitialContent: "origin", FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(), HandleConfirm: func(response string) error { - return self.c.WithWaitingStatus(self.c.Tr.PushingTagStatus, func() error { + return self.c.WithWaitingStatus(self.c.Tr.PushingTagStatus, func(task gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.PushTag) - err := self.c.Git().Tag.Push(response, tag.Name) + err := self.c.Git().Tag.Push(task, response, tag.Name) if err != nil { _ = self.c.Error(err) } diff --git a/pkg/gui/controllers/undo_controller.go b/pkg/gui/controllers/undo_controller.go index 8a9b45a959c..60eff1888d4 100644 --- a/pkg/gui/controllers/undo_controller.go +++ b/pkg/gui/controllers/undo_controller.go @@ -3,6 +3,7 @@ package controllers import ( "fmt" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" @@ -247,7 +248,7 @@ func (self *UndoController) hardResetWithAutoStash(commitSha string, options har Title: self.c.Tr.AutoStashTitle, Prompt: self.c.Tr.AutoStashPrompt, HandleConfirm: func() error { - return self.c.WithWaitingStatus(options.WaitingStatus, func() error { + return self.c.WithWaitingStatus(options.WaitingStatus, func(gocui.Task) error { if err := self.c.Git().Stash.Save(self.c.Tr.StashPrefix + commitSha); err != nil { return self.c.Error(err) } @@ -268,7 +269,7 @@ func (self *UndoController) hardResetWithAutoStash(commitSha string, options har }) } - return self.c.WithWaitingStatus(options.WaitingStatus, func() error { + return self.c.WithWaitingStatus(options.WaitingStatus, func(gocui.Task) error { return reset() }) } diff --git a/pkg/gui/file_watching.go b/pkg/gui/file_watching.go index ff04353e254..2c8addf8fa2 100644 --- a/pkg/gui/file_watching.go +++ b/pkg/gui/file_watching.go @@ -120,7 +120,10 @@ func (gui *Gui) WatchFilesForChanges() { } // only refresh if we're not already if !gui.IsRefreshingFiles { - _ = gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) + gui.c.OnUIThread(func() error { + // TODO: find out if refresh needs to be run on the UI thread + return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) + }) } // watch for errors diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 106aee7a99a..1057a85bf3a 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -130,6 +130,8 @@ type Gui struct { c *helpers.HelperCommon helpers *helpers.Helpers + + integrationTest integrationTypes.IntegrationTest } type StateAccessor struct { @@ -446,14 +448,15 @@ func NewGui( // sake of backwards compatibility. We're making use of short circuiting here ShowExtrasWindow: cmn.UserConfig.Gui.ShowCommandLog && !config.GetAppState().HideCommandLog, Mutexes: types.Mutexes{ - RefreshingFilesMutex: &deadlock.Mutex{}, - RefreshingStatusMutex: &deadlock.Mutex{}, - SyncMutex: &deadlock.Mutex{}, - LocalCommitsMutex: &deadlock.Mutex{}, - SubCommitsMutex: &deadlock.Mutex{}, - SubprocessMutex: &deadlock.Mutex{}, - PopupMutex: &deadlock.Mutex{}, - PtyMutex: &deadlock.Mutex{}, + RefreshingFilesMutex: &deadlock.Mutex{}, + RefreshingBranchesMutex: &deadlock.Mutex{}, + RefreshingStatusMutex: &deadlock.Mutex{}, + SyncMutex: &deadlock.Mutex{}, + LocalCommitsMutex: &deadlock.Mutex{}, + SubCommitsMutex: &deadlock.Mutex{}, + SubprocessMutex: &deadlock.Mutex{}, + PopupMutex: &deadlock.Mutex{}, + PtyMutex: &deadlock.Mutex{}, }, InitialDir: initialDir, } @@ -469,9 +472,10 @@ func NewGui( func() error { return gui.State.ContextMgr.Pop() }, func() types.Context { return gui.State.ContextMgr.Current() }, gui.createMenu, - func(message string, f func() error) { gui.helpers.AppStatus.WithWaitingStatus(message, f) }, + func(message string, f func(gocui.Task) error) { gui.helpers.AppStatus.WithWaitingStatus(message, f) }, func(message string) { gui.helpers.AppStatus.Toast(message) }, func() string { return gui.Views.Confirmation.TextArea.GetContent() }, + func(f func(gocui.Task)) { gui.c.OnWorker(f) }, ) guiCommon := &guiCommon{gui: gui, IPopupHandler: gui.PopupHandler} @@ -620,7 +624,8 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error { gui.c.Log.Info("starting main loop") - gui.handleTestMode(startArgs.IntegrationTest) + // setting here so we can use it in layout.go + gui.integrationTest = startArgs.IntegrationTest return gui.g.MainLoop() } @@ -716,8 +721,8 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, return false, gui.c.Error(err) } - gui.BackgroundRoutineMgr.PauseBackgroundThreads(true) - defer gui.BackgroundRoutineMgr.PauseBackgroundThreads(false) + gui.BackgroundRoutineMgr.PauseBackgroundRefreshes(true) + defer gui.BackgroundRoutineMgr.PauseBackgroundRefreshes(false) cmdErr := gui.runSubprocess(subprocess) @@ -775,37 +780,23 @@ func (gui *Gui) loadNewRepo() error { return nil } -func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) { - gui.waitForIntro.Add(len(tasks)) - done := make(chan struct{}) - - go utils.Safe(func() { - for _, task := range tasks { - task := task - go utils.Safe(func() { - if err := task(done); err != nil { - _ = gui.c.Error(err) - } - }) +func (gui *Gui) showIntroPopupMessage() { + gui.waitForIntro.Add(1) - <-done + gui.c.OnUIThread(func() error { + onConfirm := func() error { + gui.c.GetAppState().StartupPopupVersion = StartupPopupVersion + err := gui.c.SaveAppState() gui.waitForIntro.Done() + return err } - }) -} - -func (gui *Gui) showIntroPopupMessage(done chan struct{}) error { - onConfirm := func() error { - done <- struct{}{} - gui.c.GetAppState().StartupPopupVersion = StartupPopupVersion - return gui.c.SaveAppState() - } - return gui.c.Confirm(types.ConfirmOpts{ - Title: "", - Prompt: gui.c.Tr.IntroPopupMessage, - HandleConfirm: onConfirm, - HandleClose: onConfirm, + return gui.c.Confirm(types.ConfirmOpts{ + Title: "", + Prompt: gui.c.Tr.IntroPopupMessage, + HandleConfirm: onConfirm, + HandleClose: onConfirm, + }) }) } @@ -828,6 +819,10 @@ func (gui *Gui) onUIThread(f func() error) { }) } +func (gui *Gui) onWorker(f func(gocui.Task)) { + gui.g.OnWorker(f) +} + func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { return gui.helpers.WindowArrangement.GetWindowDimensions(informationStr, appStatus) } diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index 8fc7732fc56..c0d7bd46069 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -136,6 +136,10 @@ func (self *guiCommon) OnUIThread(f func() error) { self.gui.onUIThread(f) } +func (self *guiCommon) OnWorker(f func(gocui.Task)) { + self.gui.onWorker(f) +} + func (self *guiCommon) RenderToMainViews(opts types.RefreshMainOpts) error { return self.gui.refreshMainViews(opts) } diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index 824c9ed33a1..630da5b0b2e 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -18,7 +18,8 @@ import ( // this gives our integration test a way of interacting with the gui for sending keypresses // and reading state. type GuiDriver struct { - gui *Gui + gui *Gui + isIdleChan chan struct{} } var _ integrationTypes.GuiDriver = &GuiDriver{} @@ -40,6 +41,9 @@ func (self *GuiDriver) PressKey(keyStr string) { tcell.NewEventKey(tcellKey, r, tcell.ModNone), 0, ) + + // wait until lazygit is idle (i.e. all processing is done) before continuing + <-self.isIdleChan } func (self *GuiDriver) Keys() config.KeybindingConfig { @@ -71,7 +75,10 @@ func (self *GuiDriver) Fail(message string) { self.gui.g.Close() // need to give the gui time to close time.Sleep(time.Millisecond * 100) - fmt.Fprintln(os.Stderr, fullMessage) + _, err := fmt.Fprintln(os.Stderr, fullMessage) + if err != nil { + panic("Test failed. Failed writing to stderr") + } panic("Test failed") } diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index ed10fda9292..14f79cb5a90 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -114,6 +114,8 @@ func (gui *Gui) layout(g *gocui.Gui) error { return err } + gui.handleTestMode() + gui.ViewsSetup = true } @@ -211,12 +213,10 @@ func (gui *Gui) onInitialViewsCreation() error { gui.g.Mutexes.ViewsMutex.Unlock() if !gui.c.UserConfig.DisableStartupPopups { - popupTasks := []func(chan struct{}) error{} storedPopupVersion := gui.c.GetAppState().StartupPopupVersion if storedPopupVersion < StartupPopupVersion { - popupTasks = append(popupTasks, gui.showIntroPopupMessage) + gui.showIntroPopupMessage() } - gui.showInitialPopups(popupTasks) } if gui.showRecentRepos { diff --git a/pkg/gui/popup/fake_popup_handler.go b/pkg/gui/popup/fake_popup_handler.go index 95b0a3b1d9f..93c706b3c30 100644 --- a/pkg/gui/popup/fake_popup_handler.go +++ b/pkg/gui/popup/fake_popup_handler.go @@ -1,6 +1,9 @@ package popup -import "github.com/jesseduffield/lazygit/pkg/gui/types" +import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) type FakePopupHandler struct { OnErrorMsg func(message string) error @@ -30,12 +33,12 @@ func (self *FakePopupHandler) Prompt(opts types.PromptOpts) error { return self.OnPrompt(opts) } -func (self *FakePopupHandler) WithLoaderPanel(message string, f func() error) error { - return f() +func (self *FakePopupHandler) WithLoaderPanel(message string, f func(gocui.Task) error) error { + return f(gocui.NewFakeTask()) } -func (self *FakePopupHandler) WithWaitingStatus(message string, f func() error) error { - return f() +func (self *FakePopupHandler) WithWaitingStatus(message string, f func(gocui.Task) error) error { + return f(gocui.NewFakeTask()) } func (self *FakePopupHandler) Menu(opts types.CreateMenuOptions) error { diff --git a/pkg/gui/popup/popup_handler.go b/pkg/gui/popup/popup_handler.go index 633e91a55fb..1a130939778 100644 --- a/pkg/gui/popup/popup_handler.go +++ b/pkg/gui/popup/popup_handler.go @@ -9,7 +9,6 @@ import ( gctx "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sasha-s/go-deadlock" ) @@ -22,9 +21,10 @@ type PopupHandler struct { popContextFn func() error currentContextFn func() types.Context createMenuFn func(types.CreateMenuOptions) error - withWaitingStatusFn func(message string, f func() error) + withWaitingStatusFn func(message string, f func(gocui.Task) error) toastFn func(message string) getPromptInputFn func() string + onWorker func(func(gocui.Task)) } var _ types.IPopupHandler = &PopupHandler{} @@ -36,9 +36,10 @@ func NewPopupHandler( popContextFn func() error, currentContextFn func() types.Context, createMenuFn func(types.CreateMenuOptions) error, - withWaitingStatusFn func(message string, f func() error), + withWaitingStatusFn func(message string, f func(gocui.Task) error), toastFn func(message string), getPromptInputFn func() string, + onWorker func(func(gocui.Task)), ) *PopupHandler { return &PopupHandler{ Common: common, @@ -51,6 +52,7 @@ func NewPopupHandler( withWaitingStatusFn: withWaitingStatusFn, toastFn: toastFn, getPromptInputFn: getPromptInputFn, + onWorker: onWorker, } } @@ -62,7 +64,7 @@ func (self *PopupHandler) Toast(message string) { self.toastFn(message) } -func (self *PopupHandler) WithWaitingStatus(message string, f func() error) error { +func (self *PopupHandler) WithWaitingStatus(message string, f func(gocui.Task) error) error { self.withWaitingStatusFn(message, f) return nil } @@ -122,7 +124,7 @@ func (self *PopupHandler) Prompt(opts types.PromptOpts) error { }) } -func (self *PopupHandler) WithLoaderPanel(message string, f func() error) error { +func (self *PopupHandler) WithLoaderPanel(message string, f func(gocui.Task) error) error { index := 0 self.Lock() self.index++ @@ -141,8 +143,8 @@ func (self *PopupHandler) WithLoaderPanel(message string, f func() error) error return nil } - go utils.Safe(func() { - if err := f(); err != nil { + self.onWorker(func(task gocui.Task) { + if err := f(task); err != nil { self.Log.Error(err) } diff --git a/pkg/gui/services/custom_commands/handler_creator.go b/pkg/gui/services/custom_commands/handler_creator.go index 404753edc0b..4d6580b0310 100644 --- a/pkg/gui/services/custom_commands/handler_creator.go +++ b/pkg/gui/services/custom_commands/handler_creator.go @@ -6,6 +6,7 @@ import ( "text/template" "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/style" @@ -264,7 +265,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses loadingText = self.c.Tr.RunningCustomCommandStatus } - return self.c.WithWaitingStatus(loadingText, func() error { + return self.c.WithWaitingStatus(loadingText, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.CustomCommand) if customCommand.Stream { diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go index 2bd7be4f58c..4fcba4b8327 100644 --- a/pkg/gui/tasks_adapter.go +++ b/pkg/gui/tasks_adapter.go @@ -48,7 +48,7 @@ func (gui *Gui) newStringTask(view *gocui.View, str string) error { func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error { manager := gui.getManager(view) - f := func(stop chan struct{}) error { + f := func(tasks.TaskOpts) error { gui.c.SetViewContent(view, str) return nil } @@ -65,7 +65,7 @@ func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error { func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX int, originY int) error { manager := gui.getManager(view) - f := func(stop chan struct{}) error { + f := func(tasks.TaskOpts) error { gui.c.SetViewContent(view, str) _ = view.SetOrigin(originX, originY) return nil @@ -81,7 +81,7 @@ func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX in func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) error { manager := gui.getManager(view) - f := func(stop chan struct{}) error { + f := func(tasks.TaskOpts) error { gui.c.ResetViewOrigin(view) gui.c.SetViewContent(view, str) return nil @@ -130,6 +130,9 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager { func() { _ = view.SetOrigin(0, 0) }, + func() gocui.Task { + return gui.c.GocuiGui().NewTask() + }, ) gui.viewBufferManagerMap[view.Name()] = manager } diff --git a/pkg/gui/test_mode.go b/pkg/gui/test_mode.go index be46041db95..e24e922a9e0 100644 --- a/pkg/gui/test_mode.go +++ b/pkg/gui/test_mode.go @@ -7,29 +7,39 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/integration/components" - integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" "github.com/jesseduffield/lazygit/pkg/utils" ) type IntegrationTest interface { - Run(guiAdapter *GuiDriver) + Run(*GuiDriver) } -func (gui *Gui) handleTestMode(test integrationTypes.IntegrationTest) { +func (gui *Gui) handleTestMode() { + test := gui.integrationTest if os.Getenv(components.SANDBOX_ENV_VAR) == "true" { return } if test != nil { + isIdleChan := make(chan struct{}) + + gui.c.GocuiGui().AddIdleListener(isIdleChan) + + waitUntilIdle := func() { + <-isIdleChan + } + go func() { - time.Sleep(time.Millisecond * 100) + waitUntilIdle() - test.Run(&GuiDriver{gui: gui}) + test.Run(&GuiDriver{gui: gui, isIdleChan: isIdleChan}) gui.g.Update(func(*gocui.Gui) error { return gocui.ErrQuit }) + waitUntilIdle() + time.Sleep(time.Second * 1) log.Fatal("gocui should have already exited") diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 09ab040f250..4e5ef627f85 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -77,6 +77,9 @@ type IGuiCommon interface { // Only necessary to call if you're not already on the UI thread i.e. you're inside a goroutine. // All controller handlers are executed on the UI thread. OnUIThread(f func() error) + // Runs a function in a goroutine. Use this whenever you want to run a goroutine and keep track of the fact + // that lazygit is still busy. See docs/dev/Busy.md + OnWorker(f func(gocui.Task)) // returns the gocui Gui struct. There is a good chance you don't actually want to use // this struct and instead want to use another method above @@ -118,8 +121,8 @@ type IPopupHandler interface { Confirm(opts ConfirmOpts) error // Shows a popup prompting the user for input. Prompt(opts PromptOpts) error - WithLoaderPanel(message string, f func() error) error - WithWaitingStatus(message string, f func() error) error + WithLoaderPanel(message string, f func(gocui.Task) error) error + WithWaitingStatus(message string, f func(gocui.Task) error) error Menu(opts CreateMenuOptions) error Toast(message string) GetPromptInput() string @@ -214,14 +217,15 @@ type Model struct { // if you add a new mutex here be sure to instantiate it. We're using pointers to // mutexes so that we can pass the mutexes to controllers. type Mutexes struct { - RefreshingFilesMutex *deadlock.Mutex - RefreshingStatusMutex *deadlock.Mutex - SyncMutex *deadlock.Mutex - LocalCommitsMutex *deadlock.Mutex - SubCommitsMutex *deadlock.Mutex - SubprocessMutex *deadlock.Mutex - PopupMutex *deadlock.Mutex - PtyMutex *deadlock.Mutex + RefreshingFilesMutex *deadlock.Mutex + RefreshingBranchesMutex *deadlock.Mutex + RefreshingStatusMutex *deadlock.Mutex + SyncMutex *deadlock.Mutex + LocalCommitsMutex *deadlock.Mutex + SubCommitsMutex *deadlock.Mutex + SubprocessMutex *deadlock.Mutex + PopupMutex *deadlock.Mutex + PtyMutex *deadlock.Mutex } type IStateAccessor interface { diff --git a/pkg/integration/clients/go_test.go b/pkg/integration/clients/go_test.go index 29d91470831..c228898db34 100644 --- a/pkg/integration/clients/go_test.go +++ b/pkg/integration/clients/go_test.go @@ -48,9 +48,8 @@ func TestIntegration(t *testing.T) { }, false, 0, - // allowing two attempts at the test. If a test fails intermittently, - // there may be a concurrency issue that we need to resolve. - 2, + // Only allowing one attempt per test. We'll see if we get any flakiness + 1, ) assert.NoError(t, err) diff --git a/pkg/integration/components/assertion_helper.go b/pkg/integration/components/assertion_helper.go index 48cc1474141..0529e8bec7a 100644 --- a/pkg/integration/components/assertion_helper.go +++ b/pkg/integration/components/assertion_helper.go @@ -1,9 +1,6 @@ package components import ( - "os" - "time" - integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" ) @@ -11,17 +8,6 @@ type assertionHelper struct { gui integrationTypes.GuiDriver } -// milliseconds we'll wait when an assertion fails. -func retryWaitTimes() []int { - if os.Getenv("LONG_WAIT_BEFORE_FAIL") == "true" { - // CI has limited hardware, may be throttled, runs tests in parallel, etc, so we - // give it more leeway compared to when we're running things locally. - return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 2000, 4000} - } else { - return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200} - } -} - func (self *assertionHelper) matchString(matcher *TextMatcher, context string, getValue func() string) { self.assertWithRetries(func() (bool, string) { value := getValue() @@ -29,19 +15,13 @@ func (self *assertionHelper) matchString(matcher *TextMatcher, context string, g }) } +// We no longer assert with retries now that lazygit tells us when it's no longer +// busy. But I'm keeping the function in case we want to re-introduce it later. func (self *assertionHelper) assertWithRetries(test func() (bool, string)) { - var message string - for _, waitTime := range retryWaitTimes() { - time.Sleep(time.Duration(waitTime) * time.Millisecond) - - var ok bool - ok, message = test() - if ok { - return - } + ok, message := test() + if !ok { + self.fail(message) } - - self.fail(message) } func (self *assertionHelper) fail(message string) { diff --git a/pkg/integration/tests/commit/reword.go b/pkg/integration/tests/commit/reword.go index 48941b7d233..21727c4943c 100644 --- a/pkg/integration/tests/commit/reword.go +++ b/pkg/integration/tests/commit/reword.go @@ -61,6 +61,7 @@ var Reword = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Commits(). Lines( Contains(wipCommitMessage), + Contains(commitMessage), ) }, }) diff --git a/pkg/integration/tests/diff/diff_and_apply_patch.go b/pkg/integration/tests/diff/diff_and_apply_patch.go index caf2338b4a9..c0c95cc1762 100644 --- a/pkg/integration/tests/diff/diff_and_apply_patch.go +++ b/pkg/integration/tests/diff/diff_and_apply_patch.go @@ -62,7 +62,7 @@ var DiffAndApplyPatch = NewIntegrationTest(NewIntegrationTestArgs{ Tap(func() { t.ExpectPopup().Menu().Title(Equals("Diffing")).Select(Contains("Exit diff mode")).Confirm() - t.Views().Information().Content(DoesNotContain("Building patch")) + t.Views().Information().Content(Contains("Building patch")) }). Press(keys.Universal.CreatePatchOptionsMenu) diff --git a/pkg/integration/tests/file/discard_changes.go b/pkg/integration/tests/file/discard_changes.go index 435d64def63..0ddd0867517 100644 --- a/pkg/integration/tests/file/discard_changes.go +++ b/pkg/integration/tests/file/discard_changes.go @@ -8,7 +8,7 @@ import ( var DiscardChanges = NewIntegrationTest(NewIntegrationTestArgs{ Description: "Discarding all possible permutations of changed files", ExtraCmdArgs: []string{}, - Skip: true, // failing due to index.lock file being created + Skip: false, SetupConfig: func(config *config.AppConfig) { }, SetupRepo: func(shell *Shell) { @@ -22,8 +22,8 @@ var DiscardChanges = NewIntegrationTest(NewIntegrationTestArgs{ shell.RunShellCommand(`echo bothmodded > both-modded.txt && git add both-modded.txt`) shell.RunShellCommand(`echo haha > deleted-them.txt && git add deleted-them.txt`) shell.RunShellCommand(`echo haha2 > deleted-us.txt && git add deleted-us.txt`) - shell.RunShellCommand(`echo mod > modded.txt & git add modded.txt`) - shell.RunShellCommand(`echo mod > modded-staged.txt & git add modded-staged.txt`) + shell.RunShellCommand(`echo mod > modded.txt && git add modded.txt`) + shell.RunShellCommand(`echo mod > modded-staged.txt && git add modded-staged.txt`) shell.RunShellCommand(`echo del > deleted.txt && git add deleted.txt`) shell.RunShellCommand(`echo del > deleted-staged.txt && git add deleted-staged.txt`) shell.RunShellCommand(`echo change-delete > change-delete.txt && git add change-delete.txt`) diff --git a/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go b/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go index 8c9fed0a4c7..4e5fe28f6ad 100644 --- a/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go +++ b/pkg/integration/tests/interactive_rebase/squash_fixups_above_first_commit.go @@ -30,7 +30,7 @@ var SquashFixupsAboveFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{ Content(Contains("Are you sure you want to create a fixup! commit for commit")). Confirm() }). - NavigateToLine(Contains("commit 01")). + NavigateToLine(Contains("commit 01").DoesNotContain("fixup!")). Press(keys.Commits.SquashAboveCommits). Tap(func() { t.ExpectPopup().Confirmation(). diff --git a/pkg/tasks/async_handler.go b/pkg/tasks/async_handler.go index c277a11847b..6f3f41b29fd 100644 --- a/pkg/tasks/async_handler.go +++ b/pkg/tasks/async_handler.go @@ -1,7 +1,7 @@ package tasks import ( - "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/jesseduffield/gocui" "github.com/sasha-s/go-deadlock" ) @@ -18,11 +18,13 @@ type AsyncHandler struct { lastId int mutex deadlock.Mutex onReject func() + onWorker func(func(gocui.Task)) } -func NewAsyncHandler() *AsyncHandler { +func NewAsyncHandler(onWorker func(func(gocui.Task))) *AsyncHandler { return &AsyncHandler{ - mutex: deadlock.Mutex{}, + mutex: deadlock.Mutex{}, + onWorker: onWorker, } } @@ -32,7 +34,7 @@ func (self *AsyncHandler) Do(f func() func()) { id := self.currentId self.mutex.Unlock() - go utils.Safe(func() { + self.onWorker(func(gocui.Task) { after := f() self.handle(after, id) }) diff --git a/pkg/tasks/async_handler_test.go b/pkg/tasks/async_handler_test.go index b6edbec20c4..ebead7aa72e 100644 --- a/pkg/tasks/async_handler_test.go +++ b/pkg/tasks/async_handler_test.go @@ -5,6 +5,7 @@ import ( "sync" "testing" + "github.com/jesseduffield/gocui" "github.com/stretchr/testify/assert" ) @@ -12,7 +13,10 @@ func TestAsyncHandler(t *testing.T) { wg := sync.WaitGroup{} wg.Add(2) - handler := NewAsyncHandler() + onWorker := func(f func(gocui.Task)) { + go f(gocui.NewFakeTask()) + } + handler := NewAsyncHandler(onWorker) handler.onReject = func() { wg.Done() } diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index ad227fc65d2..33c46380f6d 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sasha-s/go-deadlock" @@ -48,6 +49,12 @@ type ViewBufferManager struct { refreshView func() onEndOfInput func() + // see docs/dev/Busy.md + // A gocui task is not the same thing as the tasks defined in this file. + // A gocui task simply represents the fact that lazygit is busy doing something, + // whereas the tasks in this file are about rendering content to a view. + newGocuiTask func() gocui.Task + // if the user flicks through a heap of items, with each one // spawning a process to render something to the main view, // it can slow things down quite a bit. In these situations we @@ -76,6 +83,7 @@ func NewViewBufferManager( refreshView func(), onEndOfInput func(), onNewKey func(), + newGocuiTask func() gocui.Task, ) *ViewBufferManager { return &ViewBufferManager{ Log: log, @@ -85,6 +93,7 @@ func NewViewBufferManager( onEndOfInput: onEndOfInput, readLines: make(chan LinesToRead, 1024), onNewKey: onNewKey, + newGocuiTask: newGocuiTask, } } @@ -94,13 +103,22 @@ func (self *ViewBufferManager) ReadLines(n int) { }) } -// note: onDone may be called twice -func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), prefix string, linesToRead LinesToRead, onDone func()) func(chan struct{}) error { - return func(stop chan struct{}) error { - var once sync.Once - var onDoneWrapper func() - if onDone != nil { - onDoneWrapper = func() { once.Do(onDone) } +func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), prefix string, linesToRead LinesToRead, onDoneFn func()) func(TaskOpts) error { + return func(opts TaskOpts) error { + var onDoneOnce sync.Once + var onFirstPageShownOnce sync.Once + + onFirstPageShown := func() { + onFirstPageShownOnce.Do(func() { + opts.InitialContentLoaded() + }) + } + + onDone := func() { + if onDoneFn != nil { + onDoneOnce.Do(onDoneFn) + } + onFirstPageShown() } if self.throttle { @@ -109,7 +127,8 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p } select { - case <-stop: + case <-opts.Stop: + onDone() return nil default: } @@ -119,7 +138,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p timeToStart := time.Since(startTime) go utils.Safe(func() { - <-stop + <-opts.Stop // we use the time it took to start the program as a way of checking if things // are running slow at the moment. This is admittedly a crude estimate, but // the point is that we only want to throttle when things are running slow @@ -132,9 +151,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p } // for pty's we need to call onDone here so that cmd.Wait() doesn't block forever - if onDoneWrapper != nil { - onDoneWrapper() - } + onDone() }) loadingMutex := deadlock.Mutex{} @@ -153,7 +170,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p ticker := time.NewTicker(time.Millisecond * 200) defer ticker.Stop() select { - case <-stop: + case <-opts.Stop: return case <-ticker.C: loadingMutex.Lock() @@ -169,8 +186,8 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p go utils.Safe(func() { isViewStale := true writeToView := func(content []byte) { - _, _ = self.writer.Write(content) isViewStale = true + _, _ = self.writer.Write(content) } refreshViewIfStale := func() { if isViewStale { @@ -182,12 +199,12 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p outer: for { select { - case <-stop: + case <-opts.Stop: break outer case linesToRead := <-self.readLines: for i := 0; i < linesToRead.Total; i++ { select { - case <-stop: + case <-opts.Stop: break outer default: } @@ -219,6 +236,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p } } refreshViewIfStale() + onFirstPageShown() } } @@ -231,10 +249,8 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p } } - // calling onDoneWrapper here again in case the program ended on its own accord - if onDoneWrapper != nil { - onDoneWrapper() - } + // calling this here again in case the program ended on its own accord + onDone() close(done) }) @@ -272,8 +288,30 @@ func (self *ViewBufferManager) Close() { // 1) command based, where the manager can be asked to read more lines, but the command can be killed // 2) string based, where the manager can also be asked to read more lines -func (self *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key string) error { +type TaskOpts struct { + // Channel that tells the task to stop, because another task wants to run. + Stop chan struct{} + + // Only for tasks which are long-running, where we read more lines sporadically. + // We use this to keep track of when a user's action is complete (i.e. all views + // have been refreshed to display the results of their action) + InitialContentLoaded func() +} + +func (self *ViewBufferManager) NewTask(f func(TaskOpts) error, key string) error { + gocuiTask := self.newGocuiTask() + + var completeTaskOnce sync.Once + + completeGocuiTask := func() { + completeTaskOnce.Do(func() { + gocuiTask.Done() + }) + } + go utils.Safe(func() { + defer completeGocuiTask() + self.taskIDMutex.Lock() self.newTaskID++ taskID := self.newTaskID @@ -286,11 +324,14 @@ func (self *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key str self.taskIDMutex.Unlock() self.waitingMutex.Lock() - defer self.waitingMutex.Unlock() + self.taskIDMutex.Lock() if taskID < self.newTaskID { + self.waitingMutex.Unlock() + self.taskIDMutex.Unlock() return } + self.taskIDMutex.Unlock() if self.stopCurrentTask != nil { self.stopCurrentTask() @@ -307,13 +348,13 @@ func (self *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key str self.stopCurrentTask = func() { once.Do(onStop) } - go utils.Safe(func() { - if err := f(stop); err != nil { - self.Log.Error(err) // might need an onError callback - } + self.waitingMutex.Unlock() - close(notifyStopped) - }) + if err := f(TaskOpts{Stop: stop, InitialContentLoaded: completeGocuiTask}); err != nil { + self.Log.Error(err) // might need an onError callback + } + + close(notifyStopped) }) return nil diff --git a/pkg/tasks/tasks_test.go b/pkg/tasks/tasks_test.go index ef13f6bf668..6f2edbbf4c9 100644 --- a/pkg/tasks/tasks_test.go +++ b/pkg/tasks/tasks_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/secureexec" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -26,6 +27,10 @@ func TestNewCmdTaskInstantStop(t *testing.T) { onEndOfInput, getOnEndOfInputCallCount := getCounter() onNewKey, getOnNewKeyCallCount := getCounter() onDone, getOnDoneCallCount := getCounter() + task := gocui.NewFakeTask() + newTask := func() gocui.Task { + return task + } manager := NewViewBufferManager( utils.NewDummyLog(), @@ -34,6 +39,7 @@ func TestNewCmdTaskInstantStop(t *testing.T) { refreshView, onEndOfInput, onNewKey, + newTask, ) stop := make(chan struct{}) @@ -49,7 +55,7 @@ func TestNewCmdTaskInstantStop(t *testing.T) { fn := manager.NewCmdTask(start, "prefix\n", LinesToRead{20, -1}, onDone) - _ = fn(stop) + _ = fn(TaskOpts{Stop: stop, InitialContentLoaded: func() { task.Done() }}) callCountExpectations := []struct { expected int @@ -68,6 +74,10 @@ func TestNewCmdTaskInstantStop(t *testing.T) { } } + if task.Status() != gocui.TaskStatusDone { + t.Errorf("expected task status to be 'done', got '%s'", task.FormatStatus()) + } + expectedContent := "" actualContent := writer.String() if actualContent != expectedContent { @@ -82,6 +92,10 @@ func TestNewCmdTask(t *testing.T) { onEndOfInput, getOnEndOfInputCallCount := getCounter() onNewKey, getOnNewKeyCallCount := getCounter() onDone, getOnDoneCallCount := getCounter() + task := gocui.NewFakeTask() + newTask := func() gocui.Task { + return task + } manager := NewViewBufferManager( utils.NewDummyLog(), @@ -90,6 +104,7 @@ func TestNewCmdTask(t *testing.T) { refreshView, onEndOfInput, onNewKey, + newTask, ) stop := make(chan struct{}) @@ -109,7 +124,7 @@ func TestNewCmdTask(t *testing.T) { close(stop) wg.Done() }() - _ = fn(stop) + _ = fn(TaskOpts{Stop: stop, InitialContentLoaded: func() { task.Done() }}) wg.Wait() @@ -130,6 +145,10 @@ func TestNewCmdTask(t *testing.T) { } } + if task.Status() != gocui.TaskStatusDone { + t.Errorf("expected task status to be 'done', got '%s'", task.FormatStatus()) + } + expectedContent := "prefix\ntest\n" actualContent := writer.String() if actualContent != expectedContent { @@ -208,6 +227,11 @@ func TestNewCmdTaskRefresh(t *testing.T) { lineCountsOnRefresh = append(lineCountsOnRefresh, strings.Count(writer.String(), "\n")) } + task := gocui.NewFakeTask() + newTask := func() gocui.Task { + return task + } + manager := NewViewBufferManager( utils.NewDummyLog(), writer, @@ -215,6 +239,7 @@ func TestNewCmdTaskRefresh(t *testing.T) { refreshView, func() {}, func() {}, + newTask, ) stop := make(chan struct{}) @@ -234,7 +259,7 @@ func TestNewCmdTaskRefresh(t *testing.T) { close(stop) wg.Done() }() - _ = fn(stop) + _ = fn(TaskOpts{Stop: stop, InitialContentLoaded: func() { task.Done() }}) wg.Wait() diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index 84ec8d2345a..47590f959b5 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -173,6 +173,8 @@ type Gui struct { screen tcell.Screen suspendedMutex sync.Mutex suspended bool + + taskManager *TaskManager } // NewGui returns a new Gui object with a given output mode. @@ -205,6 +207,7 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless g.gEvents = make(chan GocuiEvent, 20) g.userEvents = make(chan userEvent, 20) + g.taskManager = newTaskManager() if playRecording { g.ReplayedEvents = replayedEvents{ @@ -230,6 +233,17 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless return g, nil } +func (g *Gui) NewTask() *TaskImpl { + return g.taskManager.NewTask() +} + +// An idle listener listens for when the program is idle. This is useful for +// integration tests which can wait for the program to be idle before taking +// the next step in the test. +func (g *Gui) AddIdleListener(c chan struct{}) { + g.taskManager.addIdleListener(c) +} + // Close finalizes the library. It should be called after a successful // initialization and when gocui is not needed anymore. func (g *Gui) Close() { @@ -593,7 +607,8 @@ func getKey(key interface{}) (Key, rune, error) { // userEvent represents an event triggered by the user. type userEvent struct { - f func(*Gui) error + f func(*Gui) error + task Task } // Update executes the passed function. This method can be called safely from a @@ -602,14 +617,49 @@ type userEvent struct { // the user events queue. Given that Update spawns a goroutine, the order in // which the user events will be handled is not guaranteed. func (g *Gui) Update(f func(*Gui) error) { - go g.UpdateAsync(f) + task := g.NewTask() + + go g.updateAsyncAux(f, task) } // UpdateAsync is a version of Update that does not spawn a go routine, it can // be a bit more efficient in cases where Update is called many times like when // tailing a file. In general you should use Update() func (g *Gui) UpdateAsync(f func(*Gui) error) { - g.userEvents <- userEvent{f: f} + task := g.NewTask() + + g.updateAsyncAux(f, task) +} + +func (g *Gui) updateAsyncAux(f func(*Gui) error, task Task) { + g.userEvents <- userEvent{f: f, task: task} +} + +// Calls a function in a goroutine. Handles panics gracefully and tracks +// number of background tasks. +// Always use this when you want to spawn a goroutine and you want lazygit to +// consider itself 'busy` as it runs the code. Don't use for long-running +// background goroutines where you wouldn't want lazygit to be considered busy +// (i.e. when you wouldn't want a loader to be shown to the user) +func (g *Gui) OnWorker(f func(Task)) { + task := g.NewTask() + go func() { + g.onWorkerAux(f, task) + task.Done() + }() +} + +func (g *Gui) onWorkerAux(f func(Task), task Task) { + panicking := true + defer func() { + if panicking && Screen != nil { + Screen.Fini() + } + }() + + f(task) + + panicking = false } // A Manager is in charge of GUI's layout and can be used to build widgets. @@ -666,27 +716,42 @@ func (g *Gui) MainLoop() error { } for { - select { - case ev := <-g.gEvents: - if err := g.handleEvent(&ev); err != nil { - return err - } - case ev := <-g.userEvents: - if err := ev.f(g); err != nil { - return err - } + err := g.processEvent() + if err != nil { + return err } - if err := g.consumeevents(); err != nil { + } +} + +func (g *Gui) processEvent() error { + select { + case ev := <-g.gEvents: + task := g.NewTask() + defer func() { task.Done() }() + + if err := g.handleEvent(&ev); err != nil { return err } - if err := g.flush(); err != nil { + case ev := <-g.userEvents: + defer func() { ev.task.Done() }() + + if err := ev.f(g); err != nil { return err } } + + if err := g.processRemainingEvents(); err != nil { + return err + } + if err := g.flush(); err != nil { + return err + } + + return nil } -// consumeevents handles the remaining events in the events pool. -func (g *Gui) consumeevents() error { +// processRemainingEvents handles the remaining events in the events pool. +func (g *Gui) processRemainingEvents() error { for { select { case ev := <-g.gEvents: @@ -694,7 +759,9 @@ func (g *Gui) consumeevents() error { return err } case ev := <-g.userEvents: - if err := ev.f(g); err != nil { + err := ev.f(g) + ev.task.Done() + if err != nil { return err } default: @@ -1355,7 +1422,7 @@ func (g *Gui) StartTicking(ctx context.Context) { for _, view := range g.Views() { if view.HasLoader { - g.userEvents <- userEvent{func(g *Gui) error { return nil }} + g.UpdateAsync(func(g *Gui) error { return nil }) continue outer } } diff --git a/vendor/github.com/jesseduffield/gocui/task.go b/vendor/github.com/jesseduffield/gocui/task.go new file mode 100644 index 00000000000..ace72f4a88a --- /dev/null +++ b/vendor/github.com/jesseduffield/gocui/task.go @@ -0,0 +1,94 @@ +package gocui + +// A task represents the fact that the program is busy doing something, which +// is useful for integration tests which only want to proceed when the program +// is idle. + +type Task interface { + Done() + Pause() + Continue() + // not exporting because we don't need to + isBusy() bool +} + +type TaskImpl struct { + id int + busy bool + onDone func() + withMutex func(func()) +} + +func (self *TaskImpl) Done() { + self.onDone() +} + +func (self *TaskImpl) Pause() { + self.withMutex(func() { + self.busy = false + }) +} + +func (self *TaskImpl) Continue() { + self.withMutex(func() { + self.busy = true + }) +} + +func (self *TaskImpl) isBusy() bool { + return self.busy +} + +type TaskStatus int + +const ( + TaskStatusBusy TaskStatus = iota + TaskStatusPaused + TaskStatusDone +) + +type FakeTask struct { + status TaskStatus +} + +func NewFakeTask() *FakeTask { + return &FakeTask{ + status: TaskStatusBusy, + } +} + +func (self *FakeTask) Done() { + self.status = TaskStatusDone +} + +func (self *FakeTask) Pause() { + self.status = TaskStatusPaused +} + +func (self *FakeTask) Continue() { + self.status = TaskStatusBusy +} + +func (self *FakeTask) isBusy() bool { + return self.status == TaskStatusBusy +} + +func (self *FakeTask) Status() TaskStatus { + return self.status +} + +func (self *FakeTask) FormatStatus() string { + return formatTaskStatus(self.status) +} + +func formatTaskStatus(status TaskStatus) string { + switch status { + case TaskStatusBusy: + return "busy" + case TaskStatusPaused: + return "paused" + case TaskStatusDone: + return "done" + } + return "unknown" +} diff --git a/vendor/github.com/jesseduffield/gocui/task_manager.go b/vendor/github.com/jesseduffield/gocui/task_manager.go new file mode 100644 index 00000000000..e3c82b4d4c4 --- /dev/null +++ b/vendor/github.com/jesseduffield/gocui/task_manager.go @@ -0,0 +1,67 @@ +package gocui + +import "sync" + +// Tracks whether the program is busy (i.e. either something is happening on +// the main goroutine or a worker goroutine). Used by integration tests +// to wait until the program is idle before progressing. +type TaskManager struct { + // each of these listeners will be notified when the program goes from busy to idle + idleListeners []chan struct{} + tasks map[int]Task + // auto-incrementing id for new tasks + nextId int + + mutex sync.Mutex +} + +func newTaskManager() *TaskManager { + return &TaskManager{ + tasks: make(map[int]Task), + idleListeners: []chan struct{}{}, + } +} + +func (self *TaskManager) NewTask() *TaskImpl { + self.mutex.Lock() + defer self.mutex.Unlock() + + self.nextId++ + taskId := self.nextId + + onDone := func() { self.delete(taskId) } + task := &TaskImpl{id: taskId, busy: true, onDone: onDone, withMutex: self.withMutex} + self.tasks[taskId] = task + + return task +} + +func (self *TaskManager) addIdleListener(c chan struct{}) { + self.idleListeners = append(self.idleListeners, c) +} + +func (self *TaskManager) withMutex(f func()) { + self.mutex.Lock() + defer self.mutex.Unlock() + + f() + + // Check if all tasks are done + for _, task := range self.tasks { + if task.isBusy() { + return + } + } + + // If we get here, all tasks are done, so + // notify listeners that the program is idle + for _, listener := range self.idleListeners { + listener <- struct{}{} + } +} + +func (self *TaskManager) delete(taskId int) { + self.withMutex(func() { + delete(self.tasks, taskId) + }) +} diff --git a/vendor/golang.org/x/sys/unix/mkerrors.sh b/vendor/golang.org/x/sys/unix/mkerrors.sh index 3156462715a..0c4d14929a4 100644 --- a/vendor/golang.org/x/sys/unix/mkerrors.sh +++ b/vendor/golang.org/x/sys/unix/mkerrors.sh @@ -519,7 +519,7 @@ ccflags="$@" $2 ~ /^LOCK_(SH|EX|NB|UN)$/ || $2 ~ /^LO_(KEY|NAME)_SIZE$/ || $2 ~ /^LOOP_(CLR|CTL|GET|SET)_/ || - $2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|TCP|MCAST|EVFILT|NOTE|SHUT|PROT|MAP|MFD|T?PACKET|MSG|SCM|MCL|DT|MADV|PR|LOCAL|TCPOPT|UDP)_/ || + $2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|TCP|MCAST|EVFILT|NOTE|SHUT|PROT|MAP|MREMAP|MFD|T?PACKET|MSG|SCM|MCL|DT|MADV|PR|LOCAL|TCPOPT|UDP)_/ || $2 ~ /^NFC_(GENL|PROTO|COMM|RF|SE|DIRECTION|LLCP|SOCKPROTO)_/ || $2 ~ /^NFC_.*_(MAX)?SIZE$/ || $2 ~ /^RAW_PAYLOAD_/ || diff --git a/vendor/golang.org/x/sys/unix/mremap.go b/vendor/golang.org/x/sys/unix/mremap.go new file mode 100644 index 00000000000..86213c05d69 --- /dev/null +++ b/vendor/golang.org/x/sys/unix/mremap.go @@ -0,0 +1,40 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux +// +build linux + +package unix + +import "unsafe" + +type mremapMmapper struct { + mmapper + mremap func(oldaddr uintptr, oldlength uintptr, newlength uintptr, flags int, newaddr uintptr) (xaddr uintptr, err error) +} + +func (m *mremapMmapper) Mremap(oldData []byte, newLength int, flags int) (data []byte, err error) { + if newLength <= 0 || len(oldData) == 0 || len(oldData) != cap(oldData) || flags&MREMAP_FIXED != 0 { + return nil, EINVAL + } + + pOld := &oldData[cap(oldData)-1] + m.Lock() + defer m.Unlock() + bOld := m.active[pOld] + if bOld == nil || &bOld[0] != &oldData[0] { + return nil, EINVAL + } + newAddr, errno := m.mremap(uintptr(unsafe.Pointer(&bOld[0])), uintptr(len(bOld)), uintptr(newLength), flags, 0) + if errno != nil { + return nil, errno + } + bNew := unsafe.Slice((*byte)(unsafe.Pointer(newAddr)), newLength) + pNew := &bNew[cap(bNew)-1] + if flags&MREMAP_DONTUNMAP == 0 { + delete(m.active, pOld) + } + m.active[pNew] = bNew + return bNew, nil +} diff --git a/vendor/golang.org/x/sys/unix/syscall_linux.go b/vendor/golang.org/x/sys/unix/syscall_linux.go index 6de486befe1..39de5f1430b 100644 --- a/vendor/golang.org/x/sys/unix/syscall_linux.go +++ b/vendor/golang.org/x/sys/unix/syscall_linux.go @@ -2124,11 +2124,15 @@ func writevRacedetect(iovecs []Iovec, n int) { // mmap varies by architecture; see syscall_linux_*.go. //sys munmap(addr uintptr, length uintptr) (err error) +//sys mremap(oldaddr uintptr, oldlength uintptr, newlength uintptr, flags int, newaddr uintptr) (xaddr uintptr, err error) -var mapper = &mmapper{ - active: make(map[*byte][]byte), - mmap: mmap, - munmap: munmap, +var mapper = &mremapMmapper{ + mmapper: mmapper{ + active: make(map[*byte][]byte), + mmap: mmap, + munmap: munmap, + }, + mremap: mremap, } func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error) { @@ -2139,6 +2143,10 @@ func Munmap(b []byte) (err error) { return mapper.Munmap(b) } +func Mremap(oldData []byte, newLength int, flags int) (data []byte, err error) { + return mapper.Mremap(oldData, newLength, flags) +} + //sys Madvise(b []byte, advice int) (err error) //sys Mprotect(b []byte, prot int) (err error) //sys Mlock(b []byte) (err error) @@ -2487,7 +2495,6 @@ func Getresgid() (rgid, egid, sgid int) { // MqTimedreceive // MqTimedsend // MqUnlink -// Mremap // Msgctl // Msgget // Msgrcv diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux.go b/vendor/golang.org/x/sys/unix/zerrors_linux.go index de936b677b6..3784f402e55 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux.go @@ -493,6 +493,7 @@ const ( BPF_F_TEST_RUN_ON_CPU = 0x1 BPF_F_TEST_STATE_FREQ = 0x8 BPF_F_TEST_XDP_LIVE_FRAMES = 0x2 + BPF_F_XDP_DEV_BOUND_ONLY = 0x40 BPF_F_XDP_HAS_FRAGS = 0x20 BPF_H = 0x8 BPF_IMM = 0x0 @@ -826,9 +827,9 @@ const ( DM_UUID_FLAG = 0x4000 DM_UUID_LEN = 0x81 DM_VERSION = 0xc138fd00 - DM_VERSION_EXTRA = "-ioctl (2022-07-28)" + DM_VERSION_EXTRA = "-ioctl (2023-03-01)" DM_VERSION_MAJOR = 0x4 - DM_VERSION_MINOR = 0x2f + DM_VERSION_MINOR = 0x30 DM_VERSION_PATCHLEVEL = 0x0 DT_BLK = 0x6 DT_CHR = 0x2 @@ -1197,6 +1198,7 @@ const ( FAN_EVENT_METADATA_LEN = 0x18 FAN_EVENT_ON_CHILD = 0x8000000 FAN_FS_ERROR = 0x8000 + FAN_INFO = 0x20 FAN_MARK_ADD = 0x1 FAN_MARK_DONT_FOLLOW = 0x4 FAN_MARK_EVICTABLE = 0x200 @@ -1233,6 +1235,8 @@ const ( FAN_REPORT_PIDFD = 0x80 FAN_REPORT_TARGET_FID = 0x1000 FAN_REPORT_TID = 0x100 + FAN_RESPONSE_INFO_AUDIT_RULE = 0x1 + FAN_RESPONSE_INFO_NONE = 0x0 FAN_UNLIMITED_MARKS = 0x20 FAN_UNLIMITED_QUEUE = 0x10 FD_CLOEXEC = 0x1 @@ -1860,6 +1864,7 @@ const ( MEMWRITEOOB64 = 0xc0184d15 MFD_ALLOW_SEALING = 0x2 MFD_CLOEXEC = 0x1 + MFD_EXEC = 0x10 MFD_HUGETLB = 0x4 MFD_HUGE_16GB = 0x88000000 MFD_HUGE_16MB = 0x60000000 @@ -1875,6 +1880,7 @@ const ( MFD_HUGE_8MB = 0x5c000000 MFD_HUGE_MASK = 0x3f MFD_HUGE_SHIFT = 0x1a + MFD_NOEXEC_SEAL = 0x8 MINIX2_SUPER_MAGIC = 0x2468 MINIX2_SUPER_MAGIC2 = 0x2478 MINIX3_SUPER_MAGIC = 0x4d5a @@ -1898,6 +1904,9 @@ const ( MOUNT_ATTR_SIZE_VER0 = 0x20 MOUNT_ATTR_STRICTATIME = 0x20 MOUNT_ATTR__ATIME = 0x70 + MREMAP_DONTUNMAP = 0x4 + MREMAP_FIXED = 0x2 + MREMAP_MAYMOVE = 0x1 MSDOS_SUPER_MAGIC = 0x4d44 MSG_BATCH = 0x40000 MSG_CMSG_CLOEXEC = 0x40000000 @@ -2204,6 +2213,7 @@ const ( PACKET_USER = 0x6 PACKET_VERSION = 0xa PACKET_VNET_HDR = 0xf + PACKET_VNET_HDR_SZ = 0x18 PARITY_CRC16_PR0 = 0x2 PARITY_CRC16_PR0_CCITT = 0x4 PARITY_CRC16_PR1 = 0x3 @@ -2221,6 +2231,7 @@ const ( PERF_ATTR_SIZE_VER5 = 0x70 PERF_ATTR_SIZE_VER6 = 0x78 PERF_ATTR_SIZE_VER7 = 0x80 + PERF_ATTR_SIZE_VER8 = 0x88 PERF_AUX_FLAG_COLLISION = 0x8 PERF_AUX_FLAG_CORESIGHT_FORMAT_CORESIGHT = 0x0 PERF_AUX_FLAG_CORESIGHT_FORMAT_RAW = 0x100 @@ -2361,6 +2372,7 @@ const ( PR_FP_EXC_UND = 0x40000 PR_FP_MODE_FR = 0x1 PR_FP_MODE_FRE = 0x2 + PR_GET_AUXV = 0x41555856 PR_GET_CHILD_SUBREAPER = 0x25 PR_GET_DUMPABLE = 0x3 PR_GET_ENDIAN = 0x13 @@ -2369,6 +2381,8 @@ const ( PR_GET_FP_MODE = 0x2e PR_GET_IO_FLUSHER = 0x3a PR_GET_KEEPCAPS = 0x7 + PR_GET_MDWE = 0x42 + PR_GET_MEMORY_MERGE = 0x44 PR_GET_NAME = 0x10 PR_GET_NO_NEW_PRIVS = 0x27 PR_GET_PDEATHSIG = 0x2 @@ -2389,6 +2403,7 @@ const ( PR_MCE_KILL_GET = 0x22 PR_MCE_KILL_LATE = 0x0 PR_MCE_KILL_SET = 0x1 + PR_MDWE_REFUSE_EXEC_GAIN = 0x1 PR_MPX_DISABLE_MANAGEMENT = 0x2c PR_MPX_ENABLE_MANAGEMENT = 0x2b PR_MTE_TAG_MASK = 0x7fff8 @@ -2423,6 +2438,8 @@ const ( PR_SET_FP_MODE = 0x2d PR_SET_IO_FLUSHER = 0x39 PR_SET_KEEPCAPS = 0x8 + PR_SET_MDWE = 0x41 + PR_SET_MEMORY_MERGE = 0x43 PR_SET_MM = 0x23 PR_SET_MM_ARG_END = 0x9 PR_SET_MM_ARG_START = 0x8 @@ -2506,6 +2523,7 @@ const ( PTRACE_GETSIGMASK = 0x420a PTRACE_GET_RSEQ_CONFIGURATION = 0x420f PTRACE_GET_SYSCALL_INFO = 0x420e + PTRACE_GET_SYSCALL_USER_DISPATCH_CONFIG = 0x4211 PTRACE_INTERRUPT = 0x4207 PTRACE_KILL = 0x8 PTRACE_LISTEN = 0x4208 @@ -2536,6 +2554,7 @@ const ( PTRACE_SETREGSET = 0x4205 PTRACE_SETSIGINFO = 0x4203 PTRACE_SETSIGMASK = 0x420b + PTRACE_SET_SYSCALL_USER_DISPATCH_CONFIG = 0x4210 PTRACE_SINGLESTEP = 0x9 PTRACE_SYSCALL = 0x18 PTRACE_SYSCALL_INFO_ENTRY = 0x1 @@ -3072,7 +3091,7 @@ const ( TASKSTATS_GENL_NAME = "TASKSTATS" TASKSTATS_GENL_VERSION = 0x1 TASKSTATS_TYPE_MAX = 0x6 - TASKSTATS_VERSION = 0xd + TASKSTATS_VERSION = 0xe TCIFLUSH = 0x0 TCIOFF = 0x2 TCIOFLUSH = 0x2 @@ -3238,6 +3257,7 @@ const ( TP_STATUS_COPY = 0x2 TP_STATUS_CSUMNOTREADY = 0x8 TP_STATUS_CSUM_VALID = 0x80 + TP_STATUS_GSO_TCP = 0x100 TP_STATUS_KERNEL = 0x0 TP_STATUS_LOSING = 0x4 TP_STATUS_SENDING = 0x2 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go index 9d5352c3e45..12a9a1389ea 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go @@ -443,6 +443,7 @@ const ( TIOCSWINSZ = 0x5414 TIOCVHANGUP = 0x5437 TOSTOP = 0x100 + TPIDR2_MAGIC = 0x54504902 TUNATTACHFILTER = 0x401054d5 TUNDETACHFILTER = 0x401054d6 TUNGETDEVNETNS = 0x54e3 @@ -515,6 +516,7 @@ const ( XCASE = 0x4 XTABS = 0x1800 ZA_MAGIC = 0x54366345 + ZT_MAGIC = 0x5a544e01 _HIDIOCGRAWNAME = 0x80804804 _HIDIOCGRAWPHYS = 0x80404805 _HIDIOCGRAWUNIQ = 0x80404808 diff --git a/vendor/golang.org/x/sys/unix/zsyscall_linux.go b/vendor/golang.org/x/sys/unix/zsyscall_linux.go index 722c29a0087..7ceec233fbb 100644 --- a/vendor/golang.org/x/sys/unix/zsyscall_linux.go +++ b/vendor/golang.org/x/sys/unix/zsyscall_linux.go @@ -1868,6 +1868,17 @@ func munmap(addr uintptr, length uintptr) (err error) { // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT +func mremap(oldaddr uintptr, oldlength uintptr, newlength uintptr, flags int, newaddr uintptr) (xaddr uintptr, err error) { + r0, _, e1 := Syscall6(SYS_MREMAP, uintptr(oldaddr), uintptr(oldlength), uintptr(newlength), uintptr(flags), uintptr(newaddr), 0) + xaddr = uintptr(r0) + if e1 != 0 { + err = errnoErr(e1) + } + return +} + +// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT + func Madvise(b []byte, advice int) (err error) { var _p0 unsafe.Pointer if len(b) > 0 { diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go index 7ea465204b7..e6ed7d637d0 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go @@ -372,6 +372,7 @@ const ( SYS_LANDLOCK_CREATE_RULESET = 444 SYS_LANDLOCK_ADD_RULE = 445 SYS_LANDLOCK_RESTRICT_SELF = 446 + SYS_MEMFD_SECRET = 447 SYS_PROCESS_MRELEASE = 448 SYS_FUTEX_WAITV = 449 SYS_SET_MEMPOLICY_HOME_NODE = 450 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index 00c3b8c20f3..02e2462c8f9 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -1538,6 +1538,10 @@ const ( IFLA_GRO_MAX_SIZE = 0x3a IFLA_TSO_MAX_SIZE = 0x3b IFLA_TSO_MAX_SEGS = 0x3c + IFLA_ALLMULTI = 0x3d + IFLA_DEVLINK_PORT = 0x3e + IFLA_GSO_IPV4_MAX_SIZE = 0x3f + IFLA_GRO_IPV4_MAX_SIZE = 0x40 IFLA_PROTO_DOWN_REASON_UNSPEC = 0x0 IFLA_PROTO_DOWN_REASON_MASK = 0x1 IFLA_PROTO_DOWN_REASON_VALUE = 0x2 @@ -1968,7 +1972,7 @@ const ( NFT_MSG_GETFLOWTABLE = 0x17 NFT_MSG_DELFLOWTABLE = 0x18 NFT_MSG_GETRULE_RESET = 0x19 - NFT_MSG_MAX = 0x1a + NFT_MSG_MAX = 0x21 NFTA_LIST_UNSPEC = 0x0 NFTA_LIST_ELEM = 0x1 NFTA_HOOK_UNSPEC = 0x0 @@ -3651,7 +3655,7 @@ const ( ETHTOOL_MSG_PSE_GET = 0x24 ETHTOOL_MSG_PSE_SET = 0x25 ETHTOOL_MSG_RSS_GET = 0x26 - ETHTOOL_MSG_USER_MAX = 0x26 + ETHTOOL_MSG_USER_MAX = 0x2b ETHTOOL_MSG_KERNEL_NONE = 0x0 ETHTOOL_MSG_STRSET_GET_REPLY = 0x1 ETHTOOL_MSG_LINKINFO_GET_REPLY = 0x2 @@ -3691,7 +3695,7 @@ const ( ETHTOOL_MSG_MODULE_NTF = 0x24 ETHTOOL_MSG_PSE_GET_REPLY = 0x25 ETHTOOL_MSG_RSS_GET_REPLY = 0x26 - ETHTOOL_MSG_KERNEL_MAX = 0x26 + ETHTOOL_MSG_KERNEL_MAX = 0x2b ETHTOOL_A_HEADER_UNSPEC = 0x0 ETHTOOL_A_HEADER_DEV_INDEX = 0x1 ETHTOOL_A_HEADER_DEV_NAME = 0x2 @@ -3795,7 +3799,7 @@ const ( ETHTOOL_A_RINGS_TCP_DATA_SPLIT = 0xb ETHTOOL_A_RINGS_CQE_SIZE = 0xc ETHTOOL_A_RINGS_TX_PUSH = 0xd - ETHTOOL_A_RINGS_MAX = 0xd + ETHTOOL_A_RINGS_MAX = 0x10 ETHTOOL_A_CHANNELS_UNSPEC = 0x0 ETHTOOL_A_CHANNELS_HEADER = 0x1 ETHTOOL_A_CHANNELS_RX_MAX = 0x2 @@ -3833,14 +3837,14 @@ const ( ETHTOOL_A_COALESCE_RATE_SAMPLE_INTERVAL = 0x17 ETHTOOL_A_COALESCE_USE_CQE_MODE_TX = 0x18 ETHTOOL_A_COALESCE_USE_CQE_MODE_RX = 0x19 - ETHTOOL_A_COALESCE_MAX = 0x19 + ETHTOOL_A_COALESCE_MAX = 0x1c ETHTOOL_A_PAUSE_UNSPEC = 0x0 ETHTOOL_A_PAUSE_HEADER = 0x1 ETHTOOL_A_PAUSE_AUTONEG = 0x2 ETHTOOL_A_PAUSE_RX = 0x3 ETHTOOL_A_PAUSE_TX = 0x4 ETHTOOL_A_PAUSE_STATS = 0x5 - ETHTOOL_A_PAUSE_MAX = 0x5 + ETHTOOL_A_PAUSE_MAX = 0x6 ETHTOOL_A_PAUSE_STAT_UNSPEC = 0x0 ETHTOOL_A_PAUSE_STAT_PAD = 0x1 ETHTOOL_A_PAUSE_STAT_TX_FRAMES = 0x2 @@ -4490,7 +4494,7 @@ const ( NL80211_ATTR_MAC_HINT = 0xc8 NL80211_ATTR_MAC_MASK = 0xd7 NL80211_ATTR_MAX_AP_ASSOC_STA = 0xca - NL80211_ATTR_MAX = 0x141 + NL80211_ATTR_MAX = 0x145 NL80211_ATTR_MAX_CRIT_PROT_DURATION = 0xb4 NL80211_ATTR_MAX_CSA_COUNTERS = 0xce NL80211_ATTR_MAX_MATCH_SETS = 0x85 @@ -4719,7 +4723,7 @@ const ( NL80211_BAND_ATTR_HT_CAPA = 0x4 NL80211_BAND_ATTR_HT_MCS_SET = 0x3 NL80211_BAND_ATTR_IFTYPE_DATA = 0x9 - NL80211_BAND_ATTR_MAX = 0xb + NL80211_BAND_ATTR_MAX = 0xd NL80211_BAND_ATTR_RATES = 0x2 NL80211_BAND_ATTR_VHT_CAPA = 0x8 NL80211_BAND_ATTR_VHT_MCS_SET = 0x7 @@ -4860,7 +4864,7 @@ const ( NL80211_CMD_LEAVE_IBSS = 0x2c NL80211_CMD_LEAVE_MESH = 0x45 NL80211_CMD_LEAVE_OCB = 0x6d - NL80211_CMD_MAX = 0x98 + NL80211_CMD_MAX = 0x99 NL80211_CMD_MICHAEL_MIC_FAILURE = 0x29 NL80211_CMD_MODIFY_LINK_STA = 0x97 NL80211_CMD_NAN_MATCH = 0x78 @@ -5841,6 +5845,8 @@ const ( TUN_F_TSO6 = 0x4 TUN_F_TSO_ECN = 0x8 TUN_F_UFO = 0x10 + TUN_F_USO4 = 0x20 + TUN_F_USO6 = 0x40 ) const ( @@ -5850,9 +5856,10 @@ const ( ) const ( - VIRTIO_NET_HDR_GSO_NONE = 0x0 - VIRTIO_NET_HDR_GSO_TCPV4 = 0x1 - VIRTIO_NET_HDR_GSO_UDP = 0x3 - VIRTIO_NET_HDR_GSO_TCPV6 = 0x4 - VIRTIO_NET_HDR_GSO_ECN = 0x80 + VIRTIO_NET_HDR_GSO_NONE = 0x0 + VIRTIO_NET_HDR_GSO_TCPV4 = 0x1 + VIRTIO_NET_HDR_GSO_UDP = 0x3 + VIRTIO_NET_HDR_GSO_TCPV6 = 0x4 + VIRTIO_NET_HDR_GSO_UDP_L4 = 0x5 + VIRTIO_NET_HDR_GSO_ECN = 0x80 ) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_386.go b/vendor/golang.org/x/sys/unix/ztypes_linux_386.go index 4ecc1495cd0..6d8acbcc570 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_386.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_386.go @@ -337,6 +337,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint32 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go index 34fddff964e..59293c68841 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go @@ -350,6 +350,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_arm.go b/vendor/golang.org/x/sys/unix/ztypes_linux_arm.go index 3b14a6031f3..40cfa38c29f 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_arm.go @@ -328,6 +328,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint32 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go index 0517651ab3f..055bc4216d4 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go @@ -329,6 +329,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_loong64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_loong64.go index 3b0c5181345..f28affbc607 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_loong64.go @@ -330,6 +330,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_mips.go b/vendor/golang.org/x/sys/unix/ztypes_linux_mips.go index fccdf4dd0f4..9d71e7ccd8b 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_mips.go @@ -333,6 +333,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint32 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go index 500de8fc07d..fd5ccd332a1 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go @@ -332,6 +332,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go b/vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go index d0434cd2c6d..7704de77a2f 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go @@ -332,6 +332,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go b/vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go index 84206ba5347..df00b87571a 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go @@ -333,6 +333,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint32 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc.go b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc.go index ab078cf1f51..0942840db6e 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc.go @@ -340,6 +340,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint32 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go index 42eb2c4cefd..03487439508 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go @@ -339,6 +339,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go index 31304a4e8bb..bad06704757 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go @@ -339,6 +339,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go index c311f9612d8..9ea54b7b860 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go @@ -357,6 +357,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go b/vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go index bba3cefac1d..aa268d025cf 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go @@ -352,6 +352,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go index ad8a0138046..444045b6c58 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go @@ -334,6 +334,8 @@ type Taskstats struct { Ac_exe_inode uint64 Wpcopy_count uint64 Wpcopy_delay_total uint64 + Irq_count uint64 + Irq_delay_total uint64 } type cpuMask uint64 diff --git a/vendor/golang.org/x/sys/windows/service.go b/vendor/golang.org/x/sys/windows/service.go index c964b6848d4..c44a1b96360 100644 --- a/vendor/golang.org/x/sys/windows/service.go +++ b/vendor/golang.org/x/sys/windows/service.go @@ -218,6 +218,10 @@ type SERVICE_FAILURE_ACTIONS struct { Actions *SC_ACTION } +type SERVICE_FAILURE_ACTIONS_FLAG struct { + FailureActionsOnNonCrashFailures int32 +} + type SC_ACTION struct { Type uint32 Delay uint32 diff --git a/vendor/golang.org/x/term/term_unix.go b/vendor/golang.org/x/term/term_unix.go index a4e31ab1b29..62c2b3f41f0 100644 --- a/vendor/golang.org/x/term/term_unix.go +++ b/vendor/golang.org/x/term/term_unix.go @@ -60,7 +60,7 @@ func restore(fd int, state *State) error { func getSize(fd int) (width, height int, err error) { ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) if err != nil { - return -1, -1, err + return 0, 0, err } return int(ws.Col), int(ws.Row), nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 62261a087bf..88f05f4585b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem github.com/jesseduffield/go-git/v5/utils/merkletrie/index github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame github.com/jesseduffield/go-git/v5/utils/merkletrie/noder -# github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce +# github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b ## explicit; go 1.12 github.com/jesseduffield/gocui # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 @@ -293,17 +293,17 @@ golang.org/x/exp/slices golang.org/x/net/context golang.org/x/net/internal/socks golang.org/x/net/proxy -# golang.org/x/sys v0.9.0 +# golang.org/x/sys v0.10.0 ## explicit; go 1.17 golang.org/x/sys/cpu golang.org/x/sys/internal/unsafeheader golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows -# golang.org/x/term v0.9.0 +# golang.org/x/term v0.10.0 ## explicit; go 1.17 golang.org/x/term -# golang.org/x/text v0.10.0 +# golang.org/x/text v0.11.0 ## explicit; go 1.17 golang.org/x/text/encoding golang.org/x/text/encoding/internal/identifier