Compare commits

...
Author SHA1 Message Date
Jesse Duffield 954a4d2fbb My commit summary
My commit description
2023-09-29 10:46:02 +10:00
Stefan Haller 6da1cf87b1 Support passing -race flag to integration tests (#3019) 2023-09-25 09:14:38 +02:00
Stefan Haller 508b869773 Pass MAKECMDGOALS to make integration-test-tui
We need this to be able to pass the "-race" argument, i.e.

  make integration-test-tui -- -race
2023-09-25 09:09:41 +02:00
Stefan Haller 59cc6843e6 Print race detector logs after running a test with -race 2023-09-25 09:09:41 +02:00
Stefan Haller f108fd2236 Support -race arg when running integration tests to turn on go's race detector
For the "cli" and "tui" modes of the test runner there's a "-race" parameter to
turn it on; for running tests on CI with go test, you turn it on by setting the
environment variable LAZYGIT_RACE_DETECTOR to a non-empty value.
2023-09-25 09:09:41 +02:00
Stefan Haller 8081b59a02 Allow passing multiple flags to the cli runner
This is useful for example to pass both -slow and -debug. Since we're about to
add yet another flag in the next commit, it becomes even more important. Plus,
it makes the code a little nicer too.
2023-09-25 09:09:41 +02:00
Stefan Haller 26d180a50a Fix minor resource leak in runCmdHeadless
We still want to close the pty if the command failed.
2023-09-25 09:09:41 +02:00
Jesse Duffield 10fe872c71 Fix issue where active search inappropriately changed selected line (#3022) 2023-09-25 16:43:47 +10:00
Jesse Duffield c74448f00d Don't select current search result when showing search status
Previously there was no way to render a view's search status without also moving the cursor
to the current search match. This caused issues where we wanted to display the status
after leaving the view and coming back, or when beginning a new search from within the
view.

This commit separates the two use cases so we only move the cursor when we're actually
selecting the next search match
2023-09-25 16:37:59 +10:00
Jesse Duffield 41ab7c44a0 Use upstream branch when opening pull requests (#2693)
- **PR Description**

Should probably solve #2691

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go run scripts/cheatsheet/main.go
generate`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [x] You've read through your own file changes for silly mistakes etc
2023-09-25 11:03:15 +10:00
README-bot 3589a43682 Updated README.md 2023-09-25 00:24:22 +00:00
Jesse Duffield ad70341139 Add menu to rebase onto selected branch remote upstream (#3020) 2023-09-25 10:24:06 +10:00
AzraelSec ea93df571d feat: add a menu to rebase current branch to a target branch upstream 2023-09-24 20:13:59 +02:00
AzraelSec bc21921001 chore: rename branch upstream view options method 2023-09-22 12:19:38 +02:00
Stefan Haller 07cbb62510 Replace loader panels with waiting status (pull/push/fetch) (#2973) 2023-09-20 14:07:25 +02:00
Stefan Haller fc868cdda5 Shorten the waiting status for fast-forwarding a branch
Now that we no longer show it in a loader panel, but in the app status view,
it's awkwardly long (the loading animation is much further to the right than for
other waiting status texts). Hopefully seeing just "Fast-forwarding <branch>" is
enough to be able to tell what's happening.
2023-09-20 13:56:06 +02:00
Stefan Haller 864a9ada57 Remove unused WithLoaderPanel code 2023-09-20 13:30:49 +02:00
Stefan Haller f3e9d50d94 Use WithWaitingStatus instead of WithLoaderPanel for pull/push/fetch 2023-09-20 13:30:49 +02:00
Stefan Haller cdad0b998e Add constant for LoaderAnimationInterval
Since Loader and renderAppStatus need to agree on it, it helps for it to be a
constant in case we want to change it.
2023-09-20 13:30:49 +02:00
Stefan Haller 64012d67a9 Remove unused class FakePopupHandler 2023-09-20 13:30:49 +02:00
Stefan Haller 33d9d4dbf1 Hide waiting status during credentials prompt (#3016) 2023-09-20 13:30:15 +02:00
Stefan Haller 4c5e250ed8 Add integration test for deleting a remote branch with credentials prompt
This test is interesting because it shows a credentials prompt inside a
WithWaitingStatus. Prior to this branch this test would have hung forever.
2023-09-20 13:29:30 +02:00
Stefan Haller 1359fa14c1 When pausing a task during a waiting status, hide the status while paused
We do this for two reasons:
- when popping up a credentials prompt, it looks distracting if the waiting
  status keeps spinning while the user is typing the password
- the task that updates the waiting status periodically would keep the program
  busy, so integration tests would wait forever for the program to become idle
  again
2023-09-20 13:29:30 +02:00
Stefan Haller c222a618a8 Extract WaitingStatusHandle 2023-09-20 13:29:11 +02:00
Stefan Haller 05c32e292e Extract StatusManager.addStatus method
Avoids a bit of code duplication.
2023-09-20 11:27:50 +02:00
README-bot fb4f742416 Updated README.md 2023-09-20 06:10:06 +00:00
Jesse Duffield 276438b601 Add history for search view (#2877) 2023-09-20 16:09:53 +10:00
Karim Khaleel edec116ceb Add search history
Add search history for filterable and searchable views.
2023-09-20 08:35:41 +03:00
Stefan Haller 1aae1772ce Allow cherry-picking commits during a rebase (#3013) 2023-09-19 07:15:10 +02:00
Stefan Haller a642395e9f Allow cherry-picking commits during a rebase
This can be useful when you know that a cherry-picked commit would conflict at
the tip of your branch, but doesn't at the beginning of the branch (or
somewhere in the middle). In that case you want to be able to edit the commit
before where you want to insert the cherry-picked commits, and then paste to
insert them into the todo list at that point.
2023-09-18 10:50:19 +02:00
Stefan Haller 70bfeddc90 Add StatusCommands.IsInNormalRebase and IsInInteractiveRebase
... and implement RebaseMode in terms of these.
2023-09-18 10:50:19 +02:00
Stefan Haller e2a966443b Add a DisabledReason mechanism for menu items and keybindings (#2992) 2023-09-18 10:26:11 +02:00
Stefan Haller 0b13c3ca87 Disabled paste when there are no copied commits 2023-09-18 10:20:23 +02:00
Stefan Haller 8b6766de79 Use DisabledReason for commits panel commands 2023-09-18 10:20:23 +02:00
Stefan Haller c9371f812f Use DisabledReason for rebasing a branch onto itself 2023-09-18 10:20:23 +02:00
Stefan Haller e592d81b60 Add Enabled func to Binding 2023-09-18 10:20:23 +02:00
Stefan Haller f2f50ccf75 Use DisabledReason for upstream options items 2023-09-18 10:15:11 +02:00
Stefan Haller 75aed98c35 Use DisabledReason when deleting branches is not possible
Two cases: trying to delete the currently checked-out branch, or deleting the
upstream of a branch that doesn't have one.
2023-09-18 10:15:11 +02:00
Stefan Haller 7f9818cfa2 Add DisabledReason field to MenuItem
This is useful to disable items that are not applicable right now because of
some condition (e.g. the "delete branch" menu item when the currently
checked-out branch is selected).

When a DisabledReason is set on a menu item, we
- show it in a tooltip (below the regular tooltip of the item, if it has one)
- strike through the item's key, if it has one
- show an error message with the DisabledReason if the user tries to invoke the
  command
2023-09-18 10:15:11 +02:00
README-bot 679148449a Updated README.md 2023-09-18 08:14:59 +00:00
Stefan Haller bc00aeb10d Rename test/results to test/_results (#3012) 2023-09-18 10:14:43 +02:00
Luka Markušić 4d258bd981 Use UpstreamBranch for opening pull requests 2023-09-18 13:40:52 +10:00
Stefan Haller c465b0f2ff Rename test/results to test/_results
This prevents commands like "go test ./..." from looking into it, and it
prevents VS Code's Problems panel from showing errors about the go files in that
folder.
2023-09-15 18:04:20 +02:00
README-bot 25160b671e Updated README.md 2023-09-13 07:17:12 +00:00
Jesse Duffield 20bfa42792 Use Error method to handle commits url copy from unknown service (#3007) 2023-09-13 17:16:53 +10:00
AzraelSec 5a740e34c7 fix: use Error method to handle the commit url copy from unknown service 2023-09-12 19:06:08 +02:00
Stefan Haller 0aad599a9b Various debugging improvements (#3000) 2023-09-11 08:27:03 +02:00
Stefan Haller b6c892a08a Provide a simple way to debug an integration test 2023-09-11 08:17:58 +02:00
Stefan Haller 28d12e4e5d Add debug configuration to attach to a running lazygit process
I often find it more convenient to start a lazygit process in a terminal window
and then attach to it, rather than have VS Code launch one for me.

Note that this doesn't work with "go run main.go". It does work with "make run",
however.

Make sure there's only one lazygit process running, otherwise VS Code will open
a chooser with all the running processes to pick one from, but it's pretty much
impossible to tell which is which.
2023-09-10 11:47:25 +02:00
Stefan Haller 5d5e24a48e Change "make run" to do a build and then launch lazygit
As far as I can tell, there's not much of a difference in behavior between the
two. The advantage of doing it this way is that you can attach a debugger to the
running lazygit process; see next commit.
2023-09-10 11:47:25 +02:00
Stefan Haller de598e55a6 Hide system goroutines in callstack
I have never found a reason to see them, and they just pollute the stack window
unnecessarily.
2023-09-10 11:47:25 +02:00
Stefan Haller 0fd8392067 Format launch.json with Prettier 2023-09-10 11:47:25 +02:00
81 changed files with 2375 additions and 536 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ lazygit.exe
test/git_server/data
test/results/**
test/_results/**
oryxBuildBinary
__debug_bin
+35 -4
View File
@@ -7,7 +7,11 @@
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--debug", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
"args": [
"--debug",
"--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"
],
"hideSystemGoroutines": true,
"console": "integratedTerminal",
"presentation": {
"hidden": true
@@ -19,17 +23,44 @@
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--logs", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
"args": [
"--logs",
"--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"
],
"console": "integratedTerminal",
"presentation": {
"hidden": true
}
}
},
{
"name": "Attach to a running Lazygit",
"type": "go",
"request": "attach",
"mode": "local",
"processId": "lazygit",
"hideSystemGoroutines": true,
"console": "integratedTerminal",
},
{
// To use this, first start an integration test with the "cli" runner and
// use the -debug option; e.g.
// $ make integration-test-cli -- -debug tag/reset.go
"name": "Attach to integration test runner",
"type": "go",
"request": "attach",
"mode": "local",
"processId": "test_lazygit",
"hideSystemGoroutines": true,
"console": "integratedTerminal",
},
],
"compounds": [
{
"name": "Run with logs",
"configurations": ["Tail Lazygit logs", "Debug Lazygit"],
"configurations": [
"Tail Lazygit logs",
"Debug Lazygit"
],
"stopAll": true
}
]
+3 -3
View File
@@ -10,8 +10,8 @@ install:
go install
.PHONY: run
run:
go run main.go
run: build
./lazygit
# Run `make run-debug` in one terminal tab and `make print-log` in another to view the program and its log output side by side
.PHONY: run-debug
@@ -44,7 +44,7 @@ update-cheatsheet:
# For more details about integration test, see https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md.
.PHONY: integration-test-tui
integration-test-tui:
go run cmd/integration_test/main.go tui
go run cmd/integration_test/main.go tui $(filter-out $@,$(MAKECMDGOALS))
.PHONY: integration-test-cli
integration-test-cli:
+1 -1
View File
File diff suppressed because one or more lines are too long
+40 -14
View File
@@ -26,6 +26,29 @@ Usage:
> go run cmd/integration_test/main.go help
`
type flagInfo struct {
name string // name of the flag; can be used with "-" or "--"
flag *bool // a pointer to the variable that should be set to true when this flag is passed
}
// Takes the args that you want to parse (excluding the program name and any
// subcommands), and returns the remaining args with the flags removed
func parseFlags(args []string, flags []flagInfo) []string {
outer:
for len(args) > 0 {
for _, f := range flags {
if args[0] == "-"+f.name || args[0] == "--"+f.name {
*f.flag = true
args = args[1:]
continue outer
}
}
break
}
return args
}
func main() {
if len(os.Args) < 2 {
log.Fatal(usage)
@@ -35,23 +58,26 @@ func main() {
case "help":
fmt.Println(usage)
case "cli":
testNames := os.Args[2:]
slow := false
sandbox := false
// get the next arg if it's --slow
if len(os.Args) > 2 {
if os.Args[2] == "--slow" || os.Args[2] == "-slow" {
testNames = os.Args[3:]
slow = true
} else if os.Args[2] == "--sandbox" || os.Args[2] == "-sandbox" {
testNames = os.Args[3:]
sandbox = true
}
}
clients.RunCLI(testNames, slow, sandbox)
waitForDebugger := false
raceDetector := false
testNames := parseFlags(os.Args[2:], []flagInfo{
{"slow", &slow},
{"sandbox", &sandbox},
{"debug", &waitForDebugger},
{"race", &raceDetector},
})
clients.RunCLI(testNames, slow, sandbox, waitForDebugger, raceDetector)
case "tui":
clients.RunTUI()
raceDetector := false
remainingArgs := parseFlags(os.Args[2:], []flagInfo{
{"race", &raceDetector},
})
if len(remainingArgs) > 0 {
log.Fatal("tui only supports the -race argument.")
}
clients.RunTUI(raceDetector)
default:
log.Fatal(usage)
}
+3 -2
View File
@@ -9,13 +9,13 @@ require (
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/fsmiamoto/git-todo-parser v0.0.5
github.com/go-errors/errors v1.5.0
github.com/go-errors/errors v1.5.1
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
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.20230909074155-fc7119a39341
github.com/jesseduffield/gocui v0.3.1-0.20230925062444-7cd0d7e2a70a
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
@@ -25,6 +25,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-runewidth v0.0.15
github.com/mgutz/str v1.2.0
github.com/mitchellh/go-ps v1.0.0
github.com/pmezard/go-difflib v1.0.0
github.com/sahilm/fuzzy v0.1.0
github.com/samber/lo v1.31.0
+6 -4
View File
@@ -88,8 +88,8 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.5.0 h1:/EuijeGOu7ckFxzhkj4CXJ8JaenxK7bKUxpPYqeLHqQ=
github.com/go-errors/errors v1.5.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
@@ -179,8 +179,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.20230909074155-fc7119a39341 h1:COQ5uU+gFCQuE878cw0HcTSpKX+xLTu0/6lNTPJStG4=
github.com/jesseduffield/gocui v0.3.1-0.20230909074155-fc7119a39341/go.mod h1:trXE7RRGL2hTsv+Ntk+SHLtRobg9JE138n3Ug/X2Cf4=
github.com/jesseduffield/gocui v0.3.1-0.20230925062444-7cd0d7e2a70a h1:/Ifbvq4BWY5VKoyPnHIW5oRLiy00S3ebFtVIY2MlJm0=
github.com/jesseduffield/gocui v0.3.1-0.20230925062444-7cd0d7e2a70a/go.mod h1:trXE7RRGL2hTsv+Ntk+SHLtRobg9JE138n3Ug/X2Cf4=
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=
@@ -230,6 +230,8 @@ github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+14
View File
@@ -457,6 +457,20 @@ func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
}).Run()
}
// CherryPickCommitsDuringRebase simply prepends the given commits to the existing git-rebase-todo file
func (self *RebaseCommands) CherryPickCommitsDuringRebase(commits []*models.Commit) error {
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) daemon.TodoLine {
return daemon.TodoLine{
Action: "pick",
Commit: commit,
}
})
todo := daemon.TodoLinesToString(todoLines)
filePath := filepath.Join(self.repoPaths.worktreeGitDirPath, "rebase-merge/git-rebase-todo")
return utils.PrependStrToTodoFile(filePath, []byte(todo))
}
// we can't start an interactive rebase from the first commit without passing the
// '--root' arg
func getBaseShaOrRoot(commits []*models.Commit, index int) string {
+14 -9
View File
@@ -25,19 +25,16 @@ func NewStatusCommands(
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) {
exists, err := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"))
if err != nil {
return enums.REBASE_MODE_NONE, err
}
if exists {
ok, err := self.IsInNormalRebase()
if err == nil && ok {
return enums.REBASE_MODE_NORMAL, nil
}
exists, err = self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge"))
if exists {
ok, err = self.IsInInteractiveRebase()
if err == nil && ok {
return enums.REBASE_MODE_INTERACTIVE, err
} else {
return enums.REBASE_MODE_NONE, err
}
return enums.REBASE_MODE_NONE, err
}
func (self *StatusCommands) WorkingTreeState() enums.RebaseMode {
@@ -68,6 +65,14 @@ func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) {
return strconv.ParseBool(strings.TrimSpace(res))
}
func (self *StatusCommands) IsInNormalRebase() (bool, error) {
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"))
}
func (self *StatusCommands) IsInInteractiveRebase() (bool, error) {
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge"))
}
// IsInMergeState states whether we are still mid-merge
func (self *StatusCommands) IsInMergeState() (bool, error) {
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "MERGE_HEAD"))
+5
View File
@@ -74,6 +74,11 @@ func CopyFile(src, dst string) (err error) {
return //nolint: nakedret
}
// If I was less lazy
// I would actually think
// of a way to add some real code
// for the sake of an example
// CopyDir recursively copies a directory tree, attempting to preserve permissions.
// Source directory must exist. If destination already exists we'll clobber it.
// Symlinks are ignored and skipped.
+3 -1
View File
@@ -3,13 +3,15 @@ package context
type FilteredListViewModel[T any] struct {
*FilteredList[T]
*ListViewModel[T]
*SearchHistory
}
func NewFilteredListViewModel[T any](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] {
filteredList := NewFilteredList(getList, getFilterFields)
self := &FilteredListViewModel[T]{
FilteredList: filteredList,
FilteredList: filteredList,
SearchHistory: NewSearchHistory(),
}
listViewModel := NewListViewModel(filteredList.GetFilteredList)
+20
View File
@@ -0,0 +1,20 @@
package context
import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Maintains a list of strings that have previously been searched/filtered for
type SearchHistory struct {
history *utils.HistoryBuffer[string]
}
func NewSearchHistory() *SearchHistory {
return &SearchHistory{
history: utils.NewHistoryBuffer[string](1000),
}
}
func (self *SearchHistory) GetSearchHistory() *utils.HistoryBuffer[string] {
return self.history
}
+7
View File
@@ -90,6 +90,9 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string {
return lo.Map(menuItems, func(item *types.MenuItem, _ int) []string {
displayStrings := item.LabelColumns
if item.DisabledReason != "" {
displayStrings[0] = style.FgDefault.SetStrikethrough().Sprint(displayStrings[0])
}
if !showKeys {
return displayStrings
@@ -169,6 +172,10 @@ func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin
}
func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error {
if selectedItem != nil && selectedItem.DisabledReason != "" {
return self.c.ErrorMsg(selectedItem.DisabledReason)
}
if err := self.c.PopContext(); err != nil {
return err
}
+1
View File
@@ -9,6 +9,7 @@ import (
type RemotesContext struct {
*FilteredListViewModel[*models.Remote]
*ListContextTrait
*SearchHistory
}
var (
+29 -17
View File
@@ -9,12 +9,16 @@ import (
type SearchTrait struct {
c *ContextCommon
*SearchHistory
searchString string
}
func NewSearchTrait(c *ContextCommon) *SearchTrait {
return &SearchTrait{c: c}
return &SearchTrait{
c: c,
SearchHistory: NewSearchHistory(),
}
}
func (self *SearchTrait) GetSearchString() string {
@@ -33,20 +37,32 @@ func (self *SearchTrait) ClearSearchString() {
func (self *SearchTrait) IsSearchableContext() {}
func (self *SearchTrait) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
return func(selectedLineIdx int, index int, total int) error {
self.RenderSearchStatus(index, total)
if total != 0 {
if err := innerFunc(selectedLineIdx); err != nil {
return err
}
}
return nil
}
}
func (self *SearchTrait) RenderSearchStatus(index int, total int) {
keybindingConfig := self.c.UserConfig.Keybinding
return func(y int, index int, total int) error {
if total == 0 {
self.c.SetViewContent(
self.c.Views().Search,
fmt.Sprintf(
self.c.Tr.NoMatchesFor,
self.searchString,
theme.OptionsFgColor.Sprintf(self.c.Tr.ExitSearchMode, keybindings.Label(keybindingConfig.Universal.Return)),
),
)
return nil
}
if total == 0 {
self.c.SetViewContent(
self.c.Views().Search,
fmt.Sprintf(
self.c.Tr.NoMatchesFor,
self.searchString,
theme.OptionsFgColor.Sprintf(self.c.Tr.ExitSearchMode, keybindings.Label(keybindingConfig.Universal.Return)),
),
)
} else {
self.c.SetViewContent(
self.c.Views().Search,
fmt.Sprintf(
@@ -62,10 +78,6 @@ func (self *SearchTrait) onSelectItemWrapper(innerFunc func(int) error) func(int
),
),
)
if err := innerFunc(y); err != nil {
return err
}
return nil
}
}
@@ -152,7 +152,7 @@ func (self *BasicCommitsController) copyCommitSHAToClipboard(commit *models.Comm
func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Commit) error {
url, err := self.c.Helpers().Host.GetCommitURL(commit.Sha)
if err != nil {
return err
return self.c.Error(err)
}
self.c.LogAction(self.c.Tr.Actions.CopyCommitURLToClipboard)
+149 -120
View File
@@ -12,6 +12,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type BranchesController struct {
@@ -75,9 +76,10 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Branches.RebaseBranch),
Handler: opts.Guards.OutsideFilterMode(self.rebase),
Description: self.c.Tr.RebaseBranch,
Key: opts.GetKey(opts.Config.Branches.RebaseBranch),
Handler: opts.Guards.OutsideFilterMode(self.rebase),
Description: self.c.Tr.RebaseBranch,
GetDisabledReason: self.getDisabledReasonForRebase,
},
{
Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch),
@@ -107,7 +109,7 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
},
{
Key: opts.GetKey(opts.Config.Branches.SetUpstream),
Handler: self.checkSelected(self.setUpstream),
Handler: self.checkSelected(self.viewUpstreamOptions),
Description: self.c.Tr.ViewBranchUpstreamOptions,
Tooltip: self.c.Tr.ViewBranchUpstreamOptionsTooltip,
OpensMenu: true,
@@ -139,33 +141,56 @@ func (self *BranchesController) GetOnRenderToMain() func() error {
}
}
func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error {
options := []*types.MenuItem{
{
LabelColumns: []string{self.c.Tr.ViewDivergenceFromUpstream},
OnPress: func() error {
branch := self.context().GetSelected()
if branch == nil {
return nil
func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branch) error {
viewDivergenceItem := &types.MenuItem{
LabelColumns: []string{self.c.Tr.ViewDivergenceFromUpstream},
OnPress: func() error {
branch := self.context().GetSelected()
if branch == nil {
return nil
}
return self.c.Helpers().SubCommits.ViewSubCommits(helpers.ViewSubCommitsOpts{
Ref: branch,
TitleRef: fmt.Sprintf("%s <-> %s", branch.RefName(), branch.ShortUpstreamRefName()),
RefToShowDivergenceFrom: branch.FullUpstreamRefName(),
Context: self.context(),
ShowBranchHeads: false,
})
},
Key: 'v',
}
unsetUpstreamItem := &types.MenuItem{
LabelColumns: []string{self.c.Tr.UnsetUpstream},
OnPress: func() error {
if err := self.c.Git().Branch.UnsetUpstream(selectedBranch.Name); err != nil {
return self.c.Error(err)
}
if err := self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC,
Scope: []types.RefreshableView{
types.BRANCHES,
types.COMMITS,
},
}); err != nil {
return self.c.Error(err)
}
return nil
},
Key: 'u',
}
setUpstreamItem := &types.MenuItem{
LabelColumns: []string{self.c.Tr.SetUpstream},
OnPress: func() error {
return self.c.Helpers().Upstream.PromptForUpstreamWithoutInitialContent(selectedBranch, func(upstream string) error {
upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
if err != nil {
return self.c.Error(err)
}
if !branch.RemoteBranchStoredLocally() {
return self.c.ErrorMsg(self.c.Tr.DivergenceNoUpstream)
}
return self.c.Helpers().SubCommits.ViewSubCommits(helpers.ViewSubCommitsOpts{
Ref: branch,
TitleRef: fmt.Sprintf("%s <-> %s", branch.RefName(), branch.ShortUpstreamRefName()),
RefToShowDivergenceFrom: branch.FullUpstreamRefName(),
Context: self.context(),
ShowBranchHeads: false,
})
},
Key: 'v',
},
{
LabelColumns: []string{self.c.Tr.UnsetUpstream},
OnPress: func() error {
if err := self.c.Git().Branch.UnsetUpstream(selectedBranch.Name); err != nil {
if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil {
return self.c.Error(err)
}
if err := self.c.Refresh(types.RefreshOptions{
@@ -178,75 +203,72 @@ func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error
return self.c.Error(err)
}
return nil
},
Key: 'u',
},
{
LabelColumns: []string{self.c.Tr.SetUpstream},
OnPress: func() error {
return self.c.Helpers().Upstream.PromptForUpstreamWithoutInitialContent(selectedBranch, func(upstream string) error {
upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
if err != nil {
return self.c.Error(err)
}
if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil {
return self.c.Error(err)
}
if err := self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC,
Scope: []types.RefreshableView{
types.BRANCHES,
types.COMMITS,
},
}); err != nil {
return self.c.Error(err)
}
return nil
})
},
Key: 's',
})
},
Key: 's',
}
if selectedBranch.IsTrackingRemote() {
upstream := fmt.Sprintf("%s/%s", selectedBranch.UpstreamRemote, selectedBranch.Name)
upstreamResetOptions := utils.ResolvePlaceholderString(
self.c.Tr.ViewUpstreamResetOptions,
map[string]string{"upstream": upstream},
)
upstreamResetTooltip := utils.ResolvePlaceholderString(
self.c.Tr.ViewUpstreamResetOptionsTooltip,
map[string]string{"upstream": upstream},
)
upstream := lo.Ternary(selectedBranch.RemoteBranchStoredLocally(),
fmt.Sprintf("%s/%s", selectedBranch.UpstreamRemote, selectedBranch.Name),
self.c.Tr.UpstreamGenericName)
upstreamResetOptions := utils.ResolvePlaceholderString(
self.c.Tr.ViewUpstreamResetOptions,
map[string]string{"upstream": upstream},
)
upstreamResetTooltip := utils.ResolvePlaceholderString(
self.c.Tr.ViewUpstreamResetOptionsTooltip,
map[string]string{"upstream": upstream},
)
options = append(options, &types.MenuItem{
LabelColumns: []string{upstreamResetOptions},
OpensMenu: true,
OnPress: func() error {
if selectedBranch.RemoteBranchNotStoredLocally() {
return self.c.ErrorMsg(self.c.Tr.UpstreamNotStoredLocallyError)
}
upstreamRebaseOptions := utils.ResolvePlaceholderString(
self.c.Tr.ViewUpstreamRebaseOptions,
map[string]string{"upstream": upstream},
)
upstreamRebaseTooltip := utils.ResolvePlaceholderString(
self.c.Tr.ViewUpstreamRebaseOptionsTooltip,
map[string]string{"upstream": upstream},
)
err := self.c.Helpers().Refs.CreateGitResetMenu(upstream)
if err != nil {
return self.c.Error(err)
}
return nil
},
Tooltip: upstreamResetTooltip,
Key: 'g',
})
} else {
options = append(options, &types.MenuItem{
LabelColumns: []string{self.c.Tr.ViewUpstreamDisabledResetOptions},
OpensMenu: true,
OnPress: func() error {
return self.c.ErrorMsg(self.c.Tr.UpstreamNotSetError)
},
Tooltip: self.c.Tr.UpstreamNotSetError,
Key: 'g',
})
upstreamResetItem := &types.MenuItem{
LabelColumns: []string{upstreamResetOptions},
OpensMenu: true,
OnPress: func() error {
err := self.c.Helpers().Refs.CreateGitResetMenu(upstream)
if err != nil {
return self.c.Error(err)
}
return nil
},
Tooltip: upstreamResetTooltip,
Key: 'g',
}
upstreamRebaseItem := &types.MenuItem{
LabelColumns: []string{upstreamRebaseOptions},
OpensMenu: true,
OnPress: func() error {
if err := self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranch.ShortUpstreamRefName()); err != nil {
return self.c.Error(err)
}
return nil
},
Tooltip: upstreamRebaseTooltip,
Key: 'r',
}
if !selectedBranch.RemoteBranchStoredLocally() {
viewDivergenceItem.DisabledReason = self.c.Tr.UpstreamNotSetError
unsetUpstreamItem.DisabledReason = self.c.Tr.UpstreamNotSetError
upstreamResetItem.DisabledReason = self.c.Tr.UpstreamNotSetError
upstreamRebaseItem.DisabledReason = self.c.Tr.UpstreamNotSetError
}
options := []*types.MenuItem{
viewDivergenceItem,
unsetUpstreamItem,
setUpstreamItem,
upstreamResetItem,
upstreamRebaseItem,
}
return self.c.Menu(types.CreateMenuOptions{
@@ -296,7 +318,10 @@ func (self *BranchesController) promptToCheckoutWorktree(worktree *models.Worktr
}
func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error {
return self.createPullRequest(selectedBranch.Name, "")
if !selectedBranch.IsTrackingRemote() {
return self.c.ErrorMsg(self.c.Tr.PullRequestNoUpstream)
}
return self.createPullRequest(selectedBranch.UpstreamBranch, "")
}
func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *models.Branch) error {
@@ -468,7 +493,6 @@ func (self *BranchesController) forceDelete(branch *models.Branch) error {
}
func (self *BranchesController) delete(branch *models.Branch) error {
menuItems := []*types.MenuItem{}
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
localDeleteItem := &types.MenuItem{
@@ -479,25 +503,18 @@ func (self *BranchesController) delete(branch *models.Branch) error {
},
}
if checkedOutBranch.Name == branch.Name {
localDeleteItem = &types.MenuItem{
Label: self.c.Tr.DeleteLocalBranch,
Key: 'c',
Tooltip: self.c.Tr.CantDeleteCheckOutBranch,
OnPress: func() error {
return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch)
},
}
localDeleteItem.DisabledReason = self.c.Tr.CantDeleteCheckOutBranch
}
menuItems = append(menuItems, localDeleteItem)
if branch.IsTrackingRemote() && !branch.UpstreamGone {
menuItems = append(menuItems, &types.MenuItem{
Label: self.c.Tr.DeleteRemoteBranch,
Key: 'r',
OnPress: func() error {
return self.remoteDelete(branch)
},
})
remoteDeleteItem := &types.MenuItem{
Label: self.c.Tr.DeleteRemoteBranch,
Key: 'r',
OnPress: func() error {
return self.remoteDelete(branch)
},
}
if !branch.IsTrackingRemote() || branch.UpstreamGone {
remoteDeleteItem.DisabledReason = self.c.Tr.UpstreamNotSetError
}
menuTitle := utils.ResolvePlaceholderString(
@@ -509,7 +526,7 @@ func (self *BranchesController) delete(branch *models.Branch) error {
return self.c.Menu(types.CreateMenuOptions{
Title: menuTitle,
Items: menuItems,
Items: []*types.MenuItem{localDeleteItem, remoteDeleteItem},
})
}
@@ -523,6 +540,16 @@ func (self *BranchesController) rebase() error {
return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName)
}
func (self *BranchesController) getDisabledReasonForRebase() string {
selectedBranchName := self.context().GetSelected().Name
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name
if selectedBranchName == checkedOutBranch {
return self.c.Tr.CantRebaseOntoSelf
}
return ""
}
func (self *BranchesController) fastForward(branch *models.Branch) error {
if !branch.IsTrackingRemote() {
return self.c.ErrorMsg(self.c.Tr.FwdNoUpstream)
@@ -537,14 +564,13 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
action := self.c.Tr.Actions.FastForwardBranch
message := utils.ResolvePlaceholderString(
self.c.Tr.Fetching,
self.c.Tr.FastForwarding,
map[string]string{
"from": fmt.Sprintf("%s/%s", branch.UpstreamRemote, branch.UpstreamBranch),
"to": branch.Name,
"branch": branch.Name,
},
)
return self.c.WithLoaderPanel(message, func(task gocui.Task) error {
return self.c.WithWaitingStatus(message, func(task gocui.Task) error {
worktree, ok := self.worktreeForBranch(branch)
if ok {
self.c.LogAction(action)
@@ -655,7 +681,7 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
{
LabelColumns: fromToLabelColumns(branch.Name, self.c.Tr.DefaultBranch),
OnPress: func() error {
return self.createPullRequest(branch.Name, "")
return self.handleCreatePullRequest(branch)
},
},
{
@@ -663,7 +689,7 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
OnPress: func() error {
return self.c.Prompt(types.PromptOpts{
Title: branch.Name + " →",
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetBranchNameSuggestionsFunc(),
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteBranchesSuggestionsFunc("/"),
HandleConfirm: func(targetBranchName string) error {
return self.createPullRequest(branch.Name, targetBranchName)
},
@@ -678,7 +704,10 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
&types.MenuItem{
LabelColumns: fromToLabelColumns(checkedOutBranch.Name, selectedBranch.Name),
OnPress: func() error {
return self.createPullRequest(checkedOutBranch.Name, selectedBranch.Name)
if !checkedOutBranch.IsTrackingRemote() || !selectedBranch.IsTrackingRemote() {
return self.c.ErrorMsg(self.c.Tr.PullRequestNoUpstream)
}
return self.createPullRequest(checkedOutBranch.UpstreamBranch, selectedBranch.UpstreamBranch)
},
},
)
+1 -1
View File
@@ -800,7 +800,7 @@ func (self *FilesController) onClickSecondary(opts gocui.ViewMouseBindingOpts) e
}
func (self *FilesController) fetch() error {
return self.c.WithLoaderPanel(self.c.Tr.FetchWait, func(task gocui.Task) error {
return self.c.WithWaitingStatus(self.c.Tr.FetchingStatus, func(task gocui.Task) error {
if err := self.fetchAux(task); err != nil {
_ = self.c.Error(err)
}
@@ -5,6 +5,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/status"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type AppStatusHelper struct {
@@ -33,13 +34,32 @@ func (self *AppStatusHelper) Toast(message string) {
self.renderAppStatus()
}
// A custom task for WithWaitingStatus calls; it wraps the original one and
// hides the status whenever the task is paused, and shows it again when
// continued.
type appStatusHelperTask struct {
gocui.Task
waitingStatusHandle *status.WaitingStatusHandle
}
// poor man's version of explicitly saying that struct X implements interface Y
var _ gocui.Task = appStatusHelperTask{}
func (self appStatusHelperTask) Pause() {
self.waitingStatusHandle.Hide()
self.Task.Pause()
}
func (self appStatusHelperTask) Continue() {
self.Task.Continue()
self.waitingStatusHandle.Show()
}
// withWaitingStatus wraps a function and shows a waiting status while the function is still executing
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(task); err != nil {
self.statusMgr().WithWaitingStatus(message, self.renderAppStatus, func(waitingStatusHandle *status.WaitingStatusHandle) {
if err := f(appStatusHelperTask{task, waitingStatusHandle}); err != nil {
self.c.OnUIThread(func() error {
return self.c.Error(err)
})
@@ -58,7 +78,7 @@ func (self *AppStatusHelper) GetStatusString() string {
func (self *AppStatusHelper) renderAppStatus() {
self.c.OnWorker(func(_ gocui.Task) {
ticker := time.NewTicker(time.Millisecond * 50)
ticker := time.NewTicker(time.Millisecond * utils.LoaderAnimationInterval)
defer ticker.Stop()
for range ticker.C {
appStatus := self.statusMgr().GetStatusString()
@@ -76,6 +76,19 @@ func (self *CherryPickHelper) Paste() error {
Title: self.c.Tr.CherryPick,
Prompt: self.c.Tr.SureCherryPick,
HandleConfirm: func() error {
isInRebase, err := self.c.Git().Status.IsInInteractiveRebase()
if err != nil {
return err
}
if isInRebase {
if err := self.c.Git().Rebase.CherryPickCommitsDuringRebase(self.getData().CherryPickedCommits); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
})
}
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)
@@ -85,6 +98,10 @@ func (self *CherryPickHelper) Paste() error {
})
}
func (self *CherryPickHelper) CanPaste() bool {
return self.getData().Active()
}
func (self *CherryPickHelper) Reset() error {
self.getData().ContextKey = ""
self.getData().CherryPickedCommits = nil
@@ -141,13 +141,8 @@ func (self *ConfirmationHelper) getPopupPanelWidth() int {
}
func (self *ConfirmationHelper) prepareConfirmationPanel(
ctx goContext.Context,
opts types.ConfirmOpts,
) error {
self.c.Views().Confirmation.HasLoader = opts.HasLoader
if opts.HasLoader {
self.c.GocuiGui().StartTicking(ctx)
}
self.c.Views().Confirmation.Title = opts.Title
// for now we do not support wrapping in our editor
self.c.Views().Confirmation.Wrap = !opts.Editable
@@ -181,7 +176,7 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
self.c.Mutexes().PopupMutex.Lock()
defer self.c.Mutexes().PopupMutex.Unlock()
ctx, cancel := goContext.WithCancel(ctx)
_, cancel := goContext.WithCancel(ctx)
// we don't allow interruptions of non-loader popups in case we get stuck somehow
// e.g. a credentials popup never gets its required user input so a process hangs
@@ -198,11 +193,9 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
self.clearConfirmationViewKeyBindings()
err := self.prepareConfirmationPanel(
ctx,
types.ConfirmOpts{
Title: opts.Title,
Prompt: opts.Prompt,
HasLoader: opts.HasLoader,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Editable: opts.Editable,
Mask: opts.Mask,
@@ -333,7 +326,7 @@ func (self *ConfirmationHelper) resizeMenu() {
tooltip := ""
selectedItem := self.c.Contexts().Menu.GetSelected()
if selectedItem != nil {
tooltip = selectedItem.Tooltip
tooltip = self.TooltipForMenuItem(selectedItem)
}
tooltipHeight := getMessageHeight(true, tooltip, panelWidth) + 2 // plus 2 for the frame
_, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
@@ -382,3 +375,14 @@ func (self *ConfirmationHelper) IsPopupPanel(viewName string) bool {
func (self *ConfirmationHelper) IsPopupPanelFocused() bool {
return self.IsPopupPanel(self.c.CurrentContext().GetViewName())
}
func (self *ConfirmationHelper) TooltipForMenuItem(menuItem *types.MenuItem) string {
tooltip := menuItem.Tooltip
if menuItem.DisabledReason != "" {
if tooltip != "" {
tooltip += "\n\n"
}
tooltip += style.FgRed.Sprintf(self.c.Tr.DisabledMenuItemPrefix) + menuItem.DisabledReason
}
return tooltip
}
@@ -220,9 +220,6 @@ func (self *MergeAndRebaseHelper) PromptToContinueRebase() error {
func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
checkedOutBranch := self.refsHelper.GetCheckedOutRef().Name
if ref == checkedOutBranch {
return self.c.ErrorMsg(self.c.Tr.CantRebaseOntoSelf)
}
menuItems := []*types.MenuItem{
{
Label: self.c.Tr.SimpleRebase,
+33 -5
View File
@@ -36,7 +36,7 @@ func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) err
self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix)
promptView := self.promptView()
promptView.ClearTextArea()
promptView.TextArea.TypeString(context.GetFilter())
self.OnPromptContentChanged("")
promptView.RenderTextArea()
if err := self.c.PushContext(self.c.Contexts().Search); err != nil {
@@ -49,13 +49,13 @@ func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) err
func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) error {
state := self.searchState()
state.PrevSearchIndex = -1
state.Context = context
searchString := context.GetSearchString()
self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
promptView := self.promptView()
promptView.ClearTextArea()
promptView.TextArea.TypeString(searchString)
promptView.RenderTextArea()
if err := self.c.PushContext(self.c.Contexts().Search); err != nil {
@@ -84,7 +84,8 @@ func (self *SearchHelper) DisplaySearchStatus(context types.ISearchableContext)
state.Context = context
self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
_ = context.GetView().SelectCurrentSearchResult()
index, totalCount := context.GetView().GetSearchStatus()
context.RenderSearchStatus(index, totalCount)
}
func (self *SearchHelper) searchState() *types.SearchState {
@@ -125,13 +126,17 @@ func (self *SearchHelper) ConfirmFilter() error {
// We also do this on each keypress but we do it here again just in case
state := self.searchState()
_, ok := state.Context.(types.IFilterableContext)
context, ok := state.Context.(types.IFilterableContext)
if !ok {
self.c.Log.Warnf("Context %s is not filterable", state.Context.GetKey())
return nil
}
self.OnPromptContentChanged(self.promptContent())
filterString := self.promptContent()
if filterString != "" {
context.GetSearchHistory().Push(filterString)
}
return self.c.PopContext()
}
@@ -147,6 +152,9 @@ func (self *SearchHelper) ConfirmSearch() error {
searchString := self.promptContent()
context.SetSearchString(searchString)
if searchString != "" {
context.GetSearchHistory().Push(searchString)
}
view := context.GetView()
@@ -167,6 +175,26 @@ func (self *SearchHelper) CancelPrompt() error {
return self.c.PopContext()
}
func (self *SearchHelper) ScrollHistory(scrollIncrement int) {
state := self.searchState()
context, ok := state.Context.(types.ISearchHistoryContext)
if !ok {
return
}
states := context.GetSearchHistory()
if val, err := states.PeekAt(state.PrevSearchIndex + scrollIncrement); err == nil {
state.PrevSearchIndex += scrollIncrement
promptView := self.promptView()
promptView.ClearTextArea()
promptView.TextArea.TypeString(val)
promptView.RenderTextArea()
self.OnPromptContentChanged(val)
}
}
func (self *SearchHelper) Cancel() {
state := self.searchState()
+163 -84
View File
@@ -44,70 +44,83 @@ func NewLocalCommitsController(
func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
outsideFilterModeBindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Commits.SquashDown),
Handler: self.checkSelected(self.squashDown),
Description: self.c.Tr.SquashDown,
Key: opts.GetKey(opts.Config.Commits.SquashDown),
Handler: self.checkSelected(self.squashDown),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForSquashDown),
Description: self.c.Tr.SquashDown,
},
{
Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup),
Handler: self.checkSelected(self.fixup),
Description: self.c.Tr.FixupCommit,
Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup),
Handler: self.checkSelected(self.fixup),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForFixup),
Description: self.c.Tr.FixupCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.RenameCommit),
Handler: self.checkSelected(self.reword),
Description: self.c.Tr.RewordCommit,
Key: opts.GetKey(opts.Config.Commits.RenameCommit),
Handler: self.checkSelected(self.reword),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Reword),
Description: self.c.Tr.RewordCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor),
Handler: self.checkSelected(self.rewordEditor),
Description: self.c.Tr.RenameCommitEditor,
Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor),
Handler: self.checkSelected(self.rewordEditor),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Reword),
Description: self.c.Tr.RenameCommitEditor,
},
{
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.drop),
Description: self.c.Tr.DeleteCommit,
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.checkSelected(self.drop),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Drop),
Description: self.c.Tr.DeleteCommit,
},
{
Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.checkSelected(self.edit),
Description: self.c.Tr.EditCommit,
Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: self.checkSelected(self.edit),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Edit),
Description: self.c.Tr.EditCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.PickCommit),
Handler: self.checkSelected(self.pick),
Description: self.c.Tr.PickCommit,
Key: opts.GetKey(opts.Config.Commits.PickCommit),
Handler: self.checkSelected(self.pick),
GetDisabledReason: self.getDisabledReasonForRebaseCommandWithSelectedCommit(todo.Pick),
Description: self.c.Tr.PickCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.CreateFixupCommit),
Handler: self.checkSelected(self.createFixupCommit),
Description: self.c.Tr.CreateFixupCommitDescription,
Key: opts.GetKey(opts.Config.Commits.CreateFixupCommit),
Handler: self.checkSelected(self.createFixupCommit),
GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.CreateFixupCommitDescription,
},
{
Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits),
Handler: self.checkSelected(self.squashAllAboveFixupCommits),
Description: self.c.Tr.SquashAboveCommits,
Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits),
Handler: self.checkSelected(self.squashAllAboveFixupCommits),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForSquashAllAboveFixupCommits),
Description: self.c.Tr.SquashAboveCommits,
},
{
Key: opts.GetKey(opts.Config.Commits.MoveDownCommit),
Handler: self.checkSelected(self.moveDown),
Description: self.c.Tr.MoveDownCommit,
Key: opts.GetKey(opts.Config.Commits.MoveDownCommit),
Handler: self.checkSelected(self.moveDown),
GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.MoveDownCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.MoveUpCommit),
Handler: self.checkSelected(self.moveUp),
Description: self.c.Tr.MoveUpCommit,
Key: opts.GetKey(opts.Config.Commits.MoveUpCommit),
Handler: self.checkSelected(self.moveUp),
GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.MoveUpCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.PasteCommits),
Handler: self.paste,
Description: self.c.Tr.PasteCommits,
Key: opts.GetKey(opts.Config.Commits.PasteCommits),
Handler: self.paste,
GetDisabledReason: self.getDisabledReasonForPaste,
Description: self.c.Tr.PasteCommits,
},
{
Key: opts.GetKey(opts.Config.Commits.MarkCommitAsBaseForRebase),
Handler: self.checkSelected(self.markAsBaseCommit),
Description: self.c.Tr.MarkAsBaseCommit,
Tooltip: self.c.Tr.MarkAsBaseCommitTooltip,
Key: opts.GetKey(opts.Config.Commits.MarkCommitAsBaseForRebase),
Handler: self.checkSelected(self.markAsBaseCommit),
GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.MarkAsBaseCommit,
Tooltip: self.c.Tr.MarkAsBaseCommitTooltip,
},
// overriding these navigation keybindings because we might need to load
// more commits on demand
@@ -131,25 +144,29 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
bindings := append(outsideFilterModeBindings, []*types.Binding{
{
Key: opts.GetKey(opts.Config.Commits.AmendToCommit),
Handler: self.checkSelected(self.amendTo),
Description: self.c.Tr.AmendToCommit,
Key: opts.GetKey(opts.Config.Commits.AmendToCommit),
Handler: self.checkSelected(self.amendTo),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForAmendTo),
Description: self.c.Tr.AmendToCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.ResetCommitAuthor),
Handler: self.checkSelected(self.amendAttribute),
Description: self.c.Tr.SetResetCommitAuthor,
OpensMenu: true,
Key: opts.GetKey(opts.Config.Commits.ResetCommitAuthor),
Handler: self.checkSelected(self.amendAttribute),
GetDisabledReason: self.callGetDisabledReasonFuncWithSelectedCommit(self.getDisabledReasonForAmendTo),
Description: self.c.Tr.SetResetCommitAuthor,
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Commits.RevertCommit),
Handler: self.checkSelected(self.revert),
Description: self.c.Tr.RevertCommit,
Key: opts.GetKey(opts.Config.Commits.RevertCommit),
Handler: self.checkSelected(self.revert),
GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.RevertCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.CreateTag),
Handler: self.checkSelected(self.createTag),
Description: self.c.Tr.TagCommit,
Key: opts.GetKey(opts.Config.Commits.CreateTag),
Handler: self.checkSelected(self.createTag),
GetDisabledReason: self.disabledIfNoSelectedCommit(),
Description: self.c.Tr.TagCommit,
},
{
Key: opts.GetKey(opts.Config.Commits.OpenLogMenu),
@@ -208,10 +225,6 @@ func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts {
}
func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
}
applied, err := self.handleMidRebaseCommand(todo.Squash, commit)
if err != nil {
return err
@@ -232,11 +245,15 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
})
}
func (self *LocalCommitsController) fixup(commit *models.Commit) error {
func (self *LocalCommitsController) getDisabledReasonForSquashDown(commit *models.Commit) string {
if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
return self.c.Tr.CannotSquashOrFixupFirstCommit
}
return self.rebaseCommandEnabled(todo.Squash, commit)
}
func (self *LocalCommitsController) fixup(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Fixup, commit)
if err != nil {
return err
@@ -257,6 +274,14 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error {
})
}
func (self *LocalCommitsController) getDisabledReasonForFixup(commit *models.Commit) string {
if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
return self.c.Tr.CannotSquashOrFixupFirstCommit
}
return self.rebaseCommandEnabled(todo.Squash, commit)
}
func (self *LocalCommitsController) reword(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Reword, commit)
if err != nil {
@@ -428,34 +453,10 @@ func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) e
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) {
if commit.Action == models.ActionConflict {
return true, self.c.ErrorMsg(self.c.Tr.ChangingThisActionIsNotAllowed)
}
if !commit.IsTODO() {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
// If we are in a rebase, the only action that is allowed for
// non-todo commits is rewording the current head commit
if !(action == todo.Reword && self.isHeadCommit()) {
return true, self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
}
}
return false, nil
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == todo.Reword {
return true, self.c.ErrorMsg(self.c.Tr.RewordNotSupported)
}
if allowed := isChangeOfRebaseTodoAllowed(action); !allowed {
return true, self.c.ErrorMsg(self.c.Tr.ChangingThisActionIsNotAllowed)
}
self.c.LogAction("Update rebase TODO")
msg := utils.ResolvePlaceholderString(
@@ -476,6 +477,38 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma
})
}
func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand, commit *models.Commit) string {
if commit.Action == models.ActionConflict {
return self.c.Tr.ChangingThisActionIsNotAllowed
}
if !commit.IsTODO() {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
// If we are in a rebase, the only action that is allowed for
// non-todo commits is rewording the current head commit
if !(action == todo.Reword && self.isHeadCommit()) {
return self.c.Tr.AlreadyRebasing
}
}
return ""
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == todo.Reword {
return self.c.Tr.RewordNotSupported
}
if allowed := isChangeOfRebaseTodoAllowed(action); !allowed {
return self.c.Tr.ChangingThisActionIsNotAllowed
}
return ""
}
func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
index := self.context().GetSelectedLineIdx()
commits := self.c.Model().Commits
@@ -601,6 +634,14 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
})
}
func (self *LocalCommitsController) getDisabledReasonForAmendTo(commit *models.Commit) string {
if !self.isHeadCommit() && self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.Tr.AlreadyRebasing
}
return ""
}
func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() {
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
@@ -772,6 +813,14 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co
})
}
func (self *LocalCommitsController) getDisabledReasonForSquashAllAboveFixupCommits(commit *models.Commit) string {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
return self.c.Tr.AlreadyRebasing
}
return ""
}
func (self *LocalCommitsController) createTag(commit *models.Commit) error {
return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Sha, func() {})
}
@@ -896,13 +945,35 @@ func (self *LocalCommitsController) checkSelected(callback func(*models.Commit)
return func() error {
commit := self.context().GetSelected()
if commit == nil {
return nil
// The enabled callback should have checked for this
panic("no commit selected")
}
return callback(commit)
}
}
func (self *LocalCommitsController) callGetDisabledReasonFuncWithSelectedCommit(callback func(*models.Commit) string) func() string {
return func() string {
commit := self.context().GetSelected()
if commit == nil {
return self.c.Tr.NoCommitSelected
}
return callback(commit)
}
}
func (self *LocalCommitsController) disabledIfNoSelectedCommit() func() string {
return self.callGetDisabledReasonFuncWithSelectedCommit(func(*models.Commit) string { return "" })
}
func (self *LocalCommitsController) getDisabledReasonForRebaseCommandWithSelectedCommit(action todo.TodoCommand) func() string {
return self.callGetDisabledReasonFuncWithSelectedCommit(func(commit *models.Commit) string {
return self.rebaseCommandEnabled(action, commit)
})
}
func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error {
context := self.context()
@@ -931,6 +1002,14 @@ func (self *LocalCommitsController) paste() error {
return self.c.Helpers().CherryPick.Paste()
}
func (self *LocalCommitsController) getDisabledReasonForPaste() string {
if !self.c.Helpers().CherryPick.CanPaste() {
return self.c.Tr.NoCopiedCommits
}
return ""
}
func (self *LocalCommitsController) markAsBaseCommit(commit *models.Commit) error {
if commit.Sha == self.c.Modes().MarkedBaseCommit.GetSha() {
// Reset when invoking it again on the marked commit
+1 -1
View File
@@ -54,7 +54,7 @@ func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error {
selectedMenuItem := self.context().GetSelected()
if selectedMenuItem != nil {
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip)
self.c.Views().Tooltip.SetContent(self.c.Helpers().Confirmation.TooltipForMenuItem(selectedMenuItem))
}
return nil
}
+9 -4
View File
@@ -25,6 +25,10 @@ func (self *OptionsMenuAction) Call() error {
appendBindings := func(bindings []*types.Binding, section *types.MenuSection) {
menuItems = append(menuItems,
lo.Map(bindings, func(binding *types.Binding, _ int) *types.MenuItem {
disabledReason := ""
if binding.GetDisabledReason != nil {
disabledReason = binding.GetDisabledReason()
}
return &types.MenuItem{
OpensMenu: binding.OpensMenu,
Label: binding.Description,
@@ -33,11 +37,12 @@ func (self *OptionsMenuAction) Call() error {
return nil
}
return binding.Handler()
return self.c.IGuiCommon.CallKeybindingHandler(binding)
},
Key: binding.Key,
Tooltip: binding.Tooltip,
Section: section,
Key: binding.Key,
Tooltip: binding.Tooltip,
DisabledReason: disabledReason,
Section: section,
}
})...)
}
@@ -33,6 +33,16 @@ func (self *SearchPromptController) GetKeybindings(opts types.KeybindingsOpts) [
Modifier: gocui.ModNone,
Handler: self.cancel,
},
{
Key: opts.GetKey(opts.Config.Universal.PrevItem),
Modifier: gocui.ModNone,
Handler: self.prevHistory,
},
{
Key: opts.GetKey(opts.Config.Universal.NextItem),
Modifier: gocui.ModNone,
Handler: self.nextHistory,
},
}
}
@@ -51,3 +61,13 @@ func (self *SearchPromptController) confirm() error {
func (self *SearchPromptController) cancel() error {
return self.c.Helpers().Search.CancelPrompt()
}
func (self *SearchPromptController) prevHistory() error {
self.c.Helpers().Search.ScrollHistory(1)
return nil
}
func (self *SearchPromptController) nextHistory() error {
self.c.Helpers().Search.ScrollHistory(-1)
return nil
}
+2 -2
View File
@@ -140,7 +140,7 @@ type PullFilesOptions struct {
}
func (self *SyncController) PullAux(opts PullFilesOptions) error {
return self.c.WithLoaderPanel(self.c.Tr.PullWait, func(task gocui.Task) error {
return self.c.WithWaitingStatus(self.c.Tr.PullingStatus, func(task gocui.Task) error {
return self.pullWithLock(task, opts)
})
}
@@ -168,7 +168,7 @@ type pushOpts struct {
}
func (self *SyncController) pushAux(opts pushOpts) error {
return self.c.WithLoaderPanel(self.c.Tr.PushWait, func(task gocui.Task) error {
return self.c.WithWaitingStatus(self.c.Tr.PushingStatus, func(task gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.Push)
err := self.c.Git().Sync.Push(
task,
-1
View File
@@ -487,7 +487,6 @@ func NewGui(
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) },
func() bool { return gui.c.InDemo() },
)
+4
View File
@@ -171,6 +171,10 @@ func (self *guiCommon) KeybindingsOpts() types.KeybindingsOpts {
return self.gui.keybindingOpts()
}
func (self *guiCommon) CallKeybindingHandler(binding *types.Binding) error {
return self.gui.callKeybindingHandler(binding)
}
func (self *guiCommon) IsAnyModeActive() bool {
return self.gui.helpers.Mode.IsAnyModeActive()
}
+15 -1
View File
@@ -375,7 +375,10 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View)
}
func (gui *Gui) SetKeybinding(binding *types.Binding) error {
handler := binding.Handler
handler := func() error {
return gui.callKeybindingHandler(binding)
}
// TODO: move all mouse-ey stuff into new mouse approach
if gocui.IsMouseKey(binding.Key) {
handler = func() error {
@@ -406,3 +409,14 @@ func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error {
return gui.g.SetViewClickBinding(binding)
}
func (gui *Gui) callKeybindingHandler(binding *types.Binding) error {
disabledReason := ""
if binding.GetDisabledReason != nil {
disabledReason = binding.GetDisabledReason()
}
if disabledReason != "" {
return gui.c.ErrorMsg(disabledReason)
}
return binding.Handler()
}
-54
View File
@@ -1,54 +0,0 @@
package popup
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type FakePopupHandler struct {
OnErrorMsg func(message string) error
OnConfirm func(opts types.ConfirmOpts) error
OnPrompt func(opts types.PromptOpts) error
}
var _ types.IPopupHandler = &FakePopupHandler{}
func (self *FakePopupHandler) Error(err error) error {
return self.ErrorMsg(err.Error())
}
func (self *FakePopupHandler) ErrorMsg(message string) error {
return self.OnErrorMsg(message)
}
func (self *FakePopupHandler) Alert(title string, message string) error {
panic("not yet implemented")
}
func (self *FakePopupHandler) Confirm(opts types.ConfirmOpts) error {
return self.OnConfirm(opts)
}
func (self *FakePopupHandler) Prompt(opts types.PromptOpts) error {
return self.OnPrompt(opts)
}
func (self *FakePopupHandler) WithLoaderPanel(message string, f func(gocui.Task) error) error {
return f(gocui.NewFakeTask())
}
func (self *FakePopupHandler) WithWaitingStatus(message string, f func(gocui.Task) error) error {
return f(gocui.NewFakeTask())
}
func (self *FakePopupHandler) Menu(opts types.CreateMenuOptions) error {
panic("not yet implemented")
}
func (self *FakePopupHandler) Toast(message string) {
panic("not yet implemented")
}
func (self *FakePopupHandler) GetPromptInput() string {
panic("not yet implemented")
}
-46
View File
@@ -3,11 +3,9 @@ package popup
import (
"context"
"strings"
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/common"
gctx "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/sasha-s/go-deadlock"
@@ -25,7 +23,6 @@ type PopupHandler struct {
withWaitingStatusFn func(message string, f func(gocui.Task) error)
toastFn func(message string)
getPromptInputFn func() string
onWorker func(func(gocui.Task))
inDemo func() bool
}
@@ -41,7 +38,6 @@ func NewPopupHandler(
withWaitingStatusFn func(message string, f func(gocui.Task) error),
toastFn func(message string),
getPromptInputFn func() string,
onWorker func(func(gocui.Task)),
inDemo func() bool,
) *PopupHandler {
return &PopupHandler{
@@ -55,7 +51,6 @@ func NewPopupHandler(
withWaitingStatusFn: withWaitingStatusFn,
toastFn: toastFn,
getPromptInputFn: getPromptInputFn,
onWorker: onWorker,
inDemo: inDemo,
}
}
@@ -128,47 +123,6 @@ func (self *PopupHandler) Prompt(opts types.PromptOpts) error {
})
}
func (self *PopupHandler) WithLoaderPanel(message string, f func(gocui.Task) error) error {
index := 0
self.Lock()
self.index++
index = self.index
self.Unlock()
ctx, cancel := context.WithCancel(context.Background())
err := self.createPopupPanelFn(ctx, types.CreatePopupPanelOpts{
Prompt: message,
HasLoader: true,
})
if err != nil {
self.Log.Error(err)
cancel()
return nil
}
self.onWorker(func(task gocui.Task) {
// emulating a delay due to network latency
if self.inDemo() {
time.Sleep(500 * time.Millisecond)
}
if err := f(task); err != nil {
self.Log.Error(err)
}
cancel()
self.Lock()
if index == self.index && self.currentContextFn().GetKey() == gctx.CONFIRMATION_CONTEXT_KEY {
_ = self.popContextFn()
}
self.Unlock()
})
return nil
}
// returns the content that has currently been typed into the prompt. Useful for
// asynchronously updating the suggestions list under the prompt.
func (self *PopupHandler) GetPromptInput() string {
+41 -28
View File
@@ -16,6 +16,24 @@ type StatusManager struct {
mutex deadlock.Mutex
}
// Can be used to manipulate a waiting status while it is running (e.g. pause
// and resume it)
type WaitingStatusHandle struct {
statusManager *StatusManager
message string
renderFunc func()
id int
}
func (self *WaitingStatusHandle) Show() {
self.id = self.statusManager.addStatus(self.message, "waiting")
self.renderFunc()
}
func (self *WaitingStatusHandle) Hide() {
self.statusManager.removeStatus(self.id)
}
type appStatus struct {
message string
statusType string
@@ -26,39 +44,17 @@ func NewStatusManager() *StatusManager {
return &StatusManager{}
}
func (self *StatusManager) WithWaitingStatus(message string, f func()) {
self.mutex.Lock()
func (self *StatusManager) WithWaitingStatus(message string, renderFunc func(), f func(*WaitingStatusHandle)) {
handle := &WaitingStatusHandle{statusManager: self, message: message, renderFunc: renderFunc, id: -1}
handle.Show()
self.nextId += 1
id := self.nextId
f(handle)
newStatus := appStatus{
message: message,
statusType: "waiting",
id: id,
}
self.statuses = append([]appStatus{newStatus}, self.statuses...)
self.mutex.Unlock()
f()
self.removeStatus(id)
handle.Hide()
}
func (self *StatusManager) AddToastStatus(message string) int {
self.mutex.Lock()
defer self.mutex.Unlock()
self.nextId++
id := self.nextId
newStatus := appStatus{
message: message,
statusType: "toast",
id: id,
}
self.statuses = append([]appStatus{newStatus}, self.statuses...)
id := self.addStatus(message, "toast")
go func() {
time.Sleep(time.Second * 2)
@@ -84,6 +80,23 @@ func (self *StatusManager) HasStatus() bool {
return len(self.statuses) > 0
}
func (self *StatusManager) addStatus(message string, statusType string) int {
self.mutex.Lock()
defer self.mutex.Unlock()
self.nextId++
id := self.nextId
newStatus := appStatus{
message: message,
statusType: statusType,
id: id,
}
self.statuses = append([]appStatus{newStatus}, self.statuses...)
return id
}
func (self *StatusManager) removeStatus(id int) {
self.mutex.Lock()
defer self.mutex.Unlock()
+5 -2
View File
@@ -104,6 +104,7 @@ type IGuiCommon interface {
State() IStateAccessor
KeybindingsOpts() KeybindingsOpts
CallKeybindingHandler(binding *Binding) error
// hopefully we can remove this once we've moved all our keybinding stuff out of the gui god struct.
GetInitialKeybindingsWithCustomCommands() ([]*Binding, []*gocui.ViewMouseBinding)
@@ -133,7 +134,6 @@ type IPopupHandler interface {
Confirm(opts ConfirmOpts) error
// Shows a popup prompting the user for input.
Prompt(opts PromptOpts) 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)
@@ -165,7 +165,6 @@ type ConfirmOpts struct {
Prompt string
HandleConfirm func() error
HandleClose func() error
HasLoader bool
FindSuggestionsFunc func(string) []*Suggestion
Editable bool
Mask bool
@@ -204,6 +203,10 @@ type MenuItem struct {
// The tooltip will be displayed upon highlighting the menu item
Tooltip string
// If non-empty, show this in a tooltip, style the menu item as disabled,
// and refuse to invoke the command
DisabledReason string
// Can be used to group menu items into sections with headers. MenuItems
// with the same Section should be contiguous, and will automatically get a
// section header. If nil, the item is not part of a section.
+10
View File
@@ -4,6 +4,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock"
)
@@ -87,9 +88,16 @@ type Context interface {
HandleRenderToMain() error
}
type ISearchHistoryContext interface {
Context
GetSearchHistory() *utils.HistoryBuffer[string]
}
type IFilterableContext interface {
Context
IListPanelState
ISearchHistoryContext
SetFilter(string)
GetFilter() string
@@ -100,12 +108,14 @@ type IFilterableContext interface {
type ISearchableContext interface {
Context
ISearchHistoryContext
SetSearchString(string)
GetSearchString() string
ClearSearchString()
IsSearching() bool
IsSearchableContext()
RenderSearchStatus(int, int)
}
type DiffableContext interface {
+7
View File
@@ -25,6 +25,13 @@ type Binding struct {
// to be displayed if the keybinding is highlighted from within a menu
Tooltip string
// Function to decide whether the command is enabled, and why. If this
// returns an empty string, it is; if it returns a non-empty string, it is
// disabled and we show the given text in an error message when trying to
// invoke it. When left nil, the command is always enabled. Note that this
// function must not do expensive calls.
GetDisabledReason func() string
}
// A guard is a decorator which checks something before executing a handler
+3 -2
View File
@@ -12,11 +12,12 @@ const (
// TODO: could we remove this entirely?
type SearchState struct {
Context Context
Context Context
PrevSearchIndex int
}
func NewSearchState() *SearchState {
return &SearchState{}
return &SearchState{PrevSearchIndex: -1}
}
func (self *SearchState) SearchType() SearchType {
+4 -4
View File
@@ -72,9 +72,6 @@ func chineseTranslationSet() TranslationSet {
MergeConflictsTitle: "合并冲突",
Checkout: "检出",
NoChangedFiles: "没有更改过文件",
PullWait: "正在拉取…",
PushWait: "正在推送…",
FetchWait: "正在抓取…",
SoftReset: "软重置",
AlreadyCheckedOutBranch: "您已经检出至此分支",
SureForceCheckout: "您确定要强制检出吗?您将丢失所有本地更改",
@@ -172,7 +169,7 @@ func chineseTranslationSet() TranslationSet {
ToggleStagingPanel: `切换到其他面板`,
ReturnToFilesPanel: `返回文件面板`,
FastForward: `从上游快进此分支`,
Fetching: "抓取并快进 {{.from}} -> {{.to}} ...",
FastForwarding: "抓取并快进 {{.branch}} ...",
FoundConflictsTitle: "自动合并失败",
ViewMergeRebaseOptions: "查看 合并/变基 选项",
NotMergingOrRebasing: "您目前既不进行变基也不进行合并",
@@ -223,6 +220,9 @@ func chineseTranslationSet() TranslationSet {
AmendCommitPrompt: "您确定要使用暂存文件来修改此提交吗?",
DeleteCommitTitle: "删除提交",
DeleteCommitPrompt: "您确定要删除此提交吗?",
PullingStatus: "正在拉取",
PushingStatus: "正在推送",
FetchingStatus: "正在抓取",
SquashingStatus: "正在压缩",
FixingStatus: "正在修正",
DeletingStatus: "正在删除",
+4 -4
View File
@@ -38,9 +38,6 @@ func dutchTranslationSet() TranslationSet {
ResetFilter: "Reset commit file state filter",
MergeConflictsTitle: "Merge conflicten",
Checkout: "Uitchecken",
PullWait: "Pullen...",
PushWait: "Pushen...",
FetchWait: "Fetchen...",
SoftReset: "Zacht reset",
AlreadyCheckedOutBranch: "Je hebt deze branch al uitgecheckt",
SureForceCheckout: "Weet je zeker dat je het uitchecken wil forceren? Al je lokale verandering zullen worden verwijdert",
@@ -137,7 +134,7 @@ func dutchTranslationSet() TranslationSet {
ToggleStagingPanel: `Ga naar een ander paneel`,
ReturnToFilesPanel: `Ga terug naar het bestanden paneel`,
FastForward: `Fast-forward deze branch vanaf zijn upstream`,
Fetching: "Fetching en fast-forwarding {{.from}} -> {{.to}} ...",
FastForwarding: "Fast-forwarding {{.branch}} ...",
FoundConflictsTitle: "Conflicten!",
ViewMergeRebaseOptions: "Bekijk merge/rebase opties",
NotMergingOrRebasing: "Je bent momenteel niet aan het rebasen of mergen",
@@ -187,6 +184,9 @@ func dutchTranslationSet() TranslationSet {
AmendCommitPrompt: "Weet je zeker dat je deze commit wil wijzigen met de vorige staged bestanden?",
DeleteCommitTitle: "Verwijder commit",
DeleteCommitPrompt: "Weet je zeker dat je deze commit wil verwijderen?",
PullingStatus: "Pullen",
PushingStatus: "Pushen",
FetchingStatus: "Fetchen",
SquashingStatus: "Squashen",
FixingStatus: "Fixing up",
DeletingStatus: "Verwijderen",
+23 -15
View File
@@ -58,9 +58,6 @@ type TranslationSet struct {
MergeConflictsTitle string
Checkout string
NoChangedFiles string
PullWait string
PushWait string
FetchWait string
SoftReset string
AlreadyCheckedOutBranch string
SureForceCheckout string
@@ -187,7 +184,7 @@ type TranslationSet struct {
ToggleStagingPanel string
ReturnToFilesPanel string
FastForward string
Fetching string
FastForwarding string
FoundConflictsTitle string
ViewConflictsMenuItem string
AbortMenuItem string
@@ -224,6 +221,7 @@ type TranslationSet struct {
FwdNoUpstream string
FwdNoLocalUpstream string
FwdCommitsToPush string
PullRequestNoUpstream string
ErrorOccurred string
NoRoom string
YouAreHere string
@@ -253,6 +251,9 @@ type TranslationSet struct {
AmendCommitPrompt string
DeleteCommitTitle string
DeleteCommitPrompt string
PullingStatus string
PushingStatus string
FetchingStatus string
SquashingStatus string
FixingStatus string
DeletingStatus string
@@ -352,12 +353,13 @@ type TranslationSet struct {
SetUpstream string
UnsetUpstream string
ViewDivergenceFromUpstream string
DivergenceNoUpstream string
DivergenceSectionHeaderLocal string
DivergenceSectionHeaderRemote string
ViewUpstreamResetOptions string
ViewUpstreamResetOptionsTooltip string
ViewUpstreamDisabledResetOptions string
ViewUpstreamRebaseOptions string
ViewUpstreamRebaseOptionsTooltip string
UpstreamGenericName string
SetUpstreamTitle string
SetUpstreamMessage string
EditRemote string
@@ -405,7 +407,6 @@ type TranslationSet struct {
ViewBranchUpstreamOptions string
BranchUpstreamOptionsTitle string
ViewBranchUpstreamOptionsTooltip string
UpstreamNotStoredLocallyError string
UpstreamNotSetError string
NewGitFlowBranchPrompt string
RenameBranchWarning string
@@ -612,6 +613,9 @@ type TranslationSet struct {
MarkAsBaseCommitTooltip string
MarkedCommitMarker string
PleaseGoToURL string
DisabledMenuItemPrefix string
NoCommitSelected string
NoCopiedCommits string
Actions Actions
Bisect Bisect
Log Log
@@ -847,9 +851,6 @@ func EnglishTranslationSet() TranslationSet {
FilterUnstagedFiles: "Show only unstaged files",
ResetFilter: "Reset filter",
NoChangedFiles: "No changed files",
PullWait: "Pulling...",
PushWait: "Pushing...",
FetchWait: "Fetching...",
SoftReset: "Soft reset",
AlreadyCheckedOutBranch: "You have already checked out this branch",
SureForceCheckout: "Are you sure you want force checkout? You will lose all local changes",
@@ -979,7 +980,7 @@ func EnglishTranslationSet() TranslationSet {
ToggleStagingPanel: `Switch to other panel (staged/unstaged changes)`,
ReturnToFilesPanel: `Return to files panel`,
FastForward: `Fast-forward this branch from its upstream`,
Fetching: "Fetching and fast-forwarding {{.from}} -> {{.to}} ...",
FastForwarding: "Fast-forwarding {{.branch}}",
FoundConflictsTitle: "Conflicts!",
ViewConflictsMenuItem: "View conflicts",
AbortMenuItem: "Abort the %s",
@@ -1019,6 +1020,7 @@ func EnglishTranslationSet() TranslationSet {
FwdNoUpstream: "Cannot fast-forward a branch with no upstream",
FwdNoLocalUpstream: "Cannot fast-forward a branch whose remote is not registered locally",
FwdCommitsToPush: "Cannot fast-forward a branch with commits to push",
PullRequestNoUpstream: "Cannot open a pull request for a branch with no upstream",
ErrorOccurred: "An error occurred! Please create an issue at",
NoRoom: "Not enough room",
YouAreHere: "YOU ARE HERE",
@@ -1048,6 +1050,9 @@ func EnglishTranslationSet() TranslationSet {
AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?",
DeleteCommitTitle: "Delete commit",
DeleteCommitPrompt: "Are you sure you want to delete this commit?",
PullingStatus: "Pulling",
PushingStatus: "Pushing",
FetchingStatus: "Fetching",
SquashingStatus: "Squashing",
FixingStatus: "Fixing up",
DeletingStatus: "Deleting",
@@ -1147,12 +1152,13 @@ func EnglishTranslationSet() TranslationSet {
SetUpstream: "Set upstream of selected branch",
UnsetUpstream: "Unset upstream of selected branch",
ViewDivergenceFromUpstream: "View divergence from upstream",
DivergenceNoUpstream: "Cannot show divergence of a branch that has no (locally tracked) upstream",
DivergenceSectionHeaderLocal: "Local",
DivergenceSectionHeaderRemote: "Remote",
ViewUpstreamResetOptions: "Reset checked-out branch onto {{.upstream}}",
ViewUpstreamResetOptionsTooltip: "View options for resetting the checked-out branch onto {{upstream}}. Note: this will not reset the selected branch onto the upstream, it will reset the checked-out branch onto the upstream",
ViewUpstreamDisabledResetOptions: "Reset checked-out branch onto upstream of selected branch",
ViewUpstreamRebaseOptions: "Rebase checked-out branch onto {{.upstream}}",
ViewUpstreamRebaseOptionsTooltip: "View options for rebasing the checked-out branch onto {{upstream}}. Note: this will not rebase the selected branch onto the upstream, it will rebased the checked-out branch onto the upstream",
UpstreamGenericName: "upstream of selected branch",
SetUpstreamTitle: "Set upstream branch",
SetUpstreamMessage: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'",
EditRemote: "Edit remote",
@@ -1196,8 +1202,7 @@ func EnglishTranslationSet() TranslationSet {
RenameBranch: "Rename branch",
BranchUpstreamOptionsTitle: "Upstream options",
ViewBranchUpstreamOptionsTooltip: "View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream",
UpstreamNotStoredLocallyError: "Cannot reset to upstream branch because it is not stored locally",
UpstreamNotSetError: "The selected branch has no upstream",
UpstreamNotSetError: "The selected branch has no upstream (or the upstream is not stored locally)",
ViewBranchUpstreamOptions: "View upstream options",
NewBranchNamePrompt: "Enter new branch name for branch",
RenameBranchWarning: "This branch is tracking a remote. This action will only rename the local branch name, not the name of the remote branch. Continue?",
@@ -1403,6 +1408,9 @@ func EnglishTranslationSet() TranslationSet {
MarkAsBaseCommitTooltip: "Select a base commit for the next rebase; this will effectively perform a 'git rebase --onto'.",
MarkedCommitMarker: "↑↑↑ Will rebase from here ↑↑↑",
PleaseGoToURL: "Please go to {{.url}}",
DisabledMenuItemPrefix: "Disabled: ",
NoCommitSelected: "No commit selected",
NoCopiedCommits: "No copied commits",
Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit",
+3 -3
View File
@@ -63,9 +63,6 @@ func japaneseTranslationSet() TranslationSet {
FilterUnstagedFiles: "ステージされていないファイルのみを表示",
ResetFilter: "フィルタをリセット",
// NoChangedFiles: "No changed files",
PullWait: "Pull中...",
PushWait: "Push中...",
FetchWait: "Fetch中...",
SoftReset: "Softリセット",
AlreadyCheckedOutBranch: "ブランチはすでにチェックアウトされています。",
// SureForceCheckout: "Are you sure you want force checkout? You will lose all local changes",
@@ -227,6 +224,9 @@ func japaneseTranslationSet() TranslationSet {
AmendCommitPrompt: "ステージされたファイルで現在のコミットをamendします。よろしいですか?",
DeleteCommitTitle: "コミットを削除",
DeleteCommitPrompt: "選択されたコミットを削除します。よろしいですか?",
PullingStatus: "Pull中",
PushingStatus: "Push中",
FetchingStatus: "Fetch中",
// SquashingStatus: "Squashing",
// FixingStatus: "Fixing up",
// DeletingStatus: "Deleting",
+4 -4
View File
@@ -62,9 +62,6 @@ func koreanTranslationSet() TranslationSet {
FilterUnstagedFiles: "Stage되지 않은 파일만 표시",
ResetFilter: "필터 리셋",
NoChangedFiles: "변경된 파일이 없습니다.",
PullWait: "업데이트 중...",
PushWait: "푸시 중...",
FetchWait: "패치 중...",
SoftReset: "소프트 리셋",
AlreadyCheckedOutBranch: "브랜치가 이미 체크아웃 되었습니다",
SureForceCheckout: "강제로 체크아웃하시겠습니까? 모든 로컬 변경 사항을 잃게 됩니다.",
@@ -173,7 +170,7 @@ func koreanTranslationSet() TranslationSet {
ToggleStagingPanel: `패널 전환`,
ReturnToFilesPanel: `파일 목록으로 돌아가기`,
FastForward: `Fast-forward this branch from its upstream`,
Fetching: "Fetching and fast-forwarding {{.from}} -> {{.to}} ...",
FastForwarding: "Fast-forwarding {{.branch}} ...",
FoundConflictsTitle: "Auto-merge failed",
ViewMergeRebaseOptions: "View merge/rebase options",
NotMergingOrRebasing: "You are currently neither rebasing nor merging",
@@ -224,6 +221,9 @@ func koreanTranslationSet() TranslationSet {
AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?",
DeleteCommitTitle: "커밋 삭제",
DeleteCommitPrompt: "정말로 선택한 커밋을 삭제하시겠습니까?",
PullingStatus: "업데이트 중",
PushingStatus: "푸시 중",
FetchingStatus: "패치 중",
SquashingStatus: "Squashing",
FixingStatus: "Fixing up",
DeletingStatus: "Deleting",
+3 -3
View File
@@ -34,9 +34,6 @@ func polishTranslationSet() TranslationSet {
ResetFilter: "Resetuj filtr commitów",
Checkout: "Przełącz",
NoChangedFiles: "Brak zmienionych plików",
PullWait: "Pobieranie zmian...",
PushWait: "Wysyłanie zmian...",
FetchWait: "Pobieram...",
AlreadyCheckedOutBranch: "Już przęłączono na tą gałąź",
SureForceCheckout: "Jesteś pewny, że chcesz wymusić przełączenie? Stracisz wszystkie lokalne zmiany",
ForceCheckoutBranch: "Wymuś przełączenie gałęzi",
@@ -153,6 +150,9 @@ func polishTranslationSet() TranslationSet {
AmendCommitPrompt: "Czy na pewno chcesz poprawić ten commit plikami z poczekalni?",
DeleteCommitTitle: "Usuń commit",
DeleteCommitPrompt: "Czy na pewno usunąć ten commit?",
PullingStatus: "Pobieranie zmian",
PushingStatus: "Wysyłanie zmian",
FetchingStatus: "Pobieram",
SquashingStatus: "Spłaszczanie",
FixingStatus: "Naprawianie",
DeletingStatus: "Usuwanie",
+4 -4
View File
@@ -81,9 +81,6 @@ func RussianTranslationSet() TranslationSet {
FilterUnstagedFiles: "Показывать только непроиндексированные файлы",
ResetFilter: "Сбросить фильтр",
NoChangedFiles: "Нет изменённых файлов",
PullWait: "Получение и слияние изменении...",
PushWait: "Отправка изменении...",
FetchWait: "Получение изменении...",
SoftReset: "Мягкий сброс",
AlreadyCheckedOutBranch: "Вы уже переключились в эту ветку",
SureForceCheckout: "Вы уверены, что хотите принудительная переключить? Вы потеряете все локальные изменения",
@@ -204,7 +201,7 @@ func RussianTranslationSet() TranslationSet {
ToggleStagingPanel: `Переключиться на другую панель (проиндексированные/непроиндексированные изменения)`,
ReturnToFilesPanel: `Вернуться к панели файлов`,
FastForward: `Перемотать эту ветку вперёд из её upstream-ветки`,
Fetching: "Получить изменения и перемотать вперёд {{.from}} -> {{.to}} ...",
FastForwarding: "Получить изменения и перемотать вперёд {{.branch}} ...",
FoundConflictsTitle: "Конфликты!",
ViewConflictsMenuItem: "Просмотр конфликтов",
AbortMenuItem: "Прервать %s",
@@ -268,6 +265,9 @@ func RussianTranslationSet() TranslationSet {
AmendCommitPrompt: "Вы уверены, что хотите править этот коммит проиндексированными файлами?",
DeleteCommitTitle: "Удалить коммит",
DeleteCommitPrompt: "Вы уверены, что хотите удалить этот коммит?",
PullingStatus: "Получение и слияние изменении",
PushingStatus: "Отправка изменении",
FetchingStatus: "Получение изменении",
SquashingStatus: "Объединение коммитов",
FixingStatus: "Объединение коммитов, отбросив сообщение коммита",
DeletingStatus: "Удаление",
+4 -4
View File
@@ -114,9 +114,6 @@ func traditionalChineseTranslationSet() TranslationSet {
FilterUnstagedFiles: "僅顯示未預存的檔案",
ResetFilter: "重設篩選",
NoChangedFiles: "沒有變更的檔案",
PullWait: "拉取...",
PushWait: "推送...",
FetchWait: "擷取...",
SoftReset: "軟重設",
AlreadyCheckedOutBranch: "你已經檢出這個分支了",
SureForceCheckout: "你確定要強制檢出嗎?這將會使你失去本地的所有更改",
@@ -237,7 +234,7 @@ func traditionalChineseTranslationSet() TranslationSet {
ToggleStagingPanel: `切換至另一個面板 (已預存/未預存更改)`,
ReturnToFilesPanel: `返回檔案面板`,
FastForward: `從上游快進此分支`,
Fetching: "{{.from}} -> {{.to}} 的擷取和快進中...",
FastForwarding: "{{.branch}} 的擷取和快進中...",
FoundConflictsTitle: "自動合併失敗",
ViewMergeRebaseOptions: "查看合併/變基選項",
NotMergingOrRebasing: "你當前既不在變基也不在合併中",
@@ -299,6 +296,9 @@ func traditionalChineseTranslationSet() TranslationSet {
AmendCommitPrompt: "你確定要使用預存的檔案修正此提交嗎?",
DeleteCommitTitle: "刪除提交",
DeleteCommitPrompt: "你確定要刪除此提交嗎?",
PullingStatus: "拉取",
PushingStatus: "推送",
FetchingStatus: "擷取",
SquashingStatus: "壓縮中",
FixingStatus: "修復中",
DeletingStatus: "刪除中",
+10 -1
View File
@@ -50,7 +50,7 @@ The name of a test is based on its path, so the name of the test at `pkg/integra
You can pass the INPUT_DELAY env var to the test runner in order to set a delay in milliseconds between keypresses or mouse clicks, which helps for watching a test at a realistic speed to understand what it's doing. Or you can pass the '--slow' flag which sets a pre-set 'slow' key delay. In the tui you can press 't' to run the test in slow mode.
The resultant repo will be stored in `test/results`, so if you're not sure what went wrong you can go there and inspect the repo.
The resultant repo will be stored in `test/_results`, so if you're not sure what went wrong you can go there and inspect the repo.
### Running tests in VSCode
@@ -61,6 +61,15 @@ If you've opened an integration test file in your editor you can run that file b
The test will run in a VSCode terminal:
![image](https://user-images.githubusercontent.com/8456633/201500446-b87abf11-9653-438f-8a9a-e0bf8abdb7ee.png)
### Debugging tests
Debugging an integration test is possible in two ways:
1. Use the -debug option of the integration test runner's "cli" command, e.g. `go run cmd/integration_test/main.go cli -debug tag/reset.go`
2. Select a test in the "tui" runner and hit "d" to debug it.
In both cases the test runner will print to the console that it is waiting for a debugger to attach, so now you need to tell your debugger to attach to a running process with the name "test_lazygit". If you are using Visual Studio Code, an easy way to do that is to use the "Attach to integration test runner" debug configuration. The test runner will resume automatically when it detects that a debugger was attached. Don't forget to set a breakpoint in the code that you want to step through, otherwise the test will just finish (i.e. it doesn't stop in the debugger automatically).
### Sandbox mode
Say you want to do a manual test of how lazygit handles merge-conflicts, but you can't be bothered actually finding a way to create merge conflicts in a repo. To make your life easier, you can simply run a merge-conflicts test in sandbox mode, meaning the setup step is run for you, and then instead of the test driving the lazygit session, you're allowed to drive it yourself.
+8 -3
View File
@@ -23,7 +23,7 @@ import (
// If invoked directly, you can specify tests to run by passing their names as positional arguments
func RunCLI(testNames []string, slow bool, sandbox bool) {
func RunCLI(testNames []string, slow bool, sandbox bool, waitForDebugger bool, raceDetector bool) {
inputDelay := tryConvert(os.Getenv("INPUT_DELAY"), 0)
if slow {
inputDelay = SLOW_INPUT_DELAY
@@ -35,6 +35,8 @@ func RunCLI(testNames []string, slow bool, sandbox bool) {
runCmdInTerminal,
runAndPrintFatalError,
sandbox,
waitForDebugger,
raceDetector,
inputDelay,
1,
)
@@ -86,12 +88,15 @@ outer:
return testsToRun
}
func runCmdInTerminal(cmd *exec.Cmd) error {
func runCmdInTerminal(cmd *exec.Cmd) (int, error) {
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
return cmd.Run()
if err := cmd.Start(); err != nil {
return -1, err
}
return cmd.Process.Pid, cmd.Wait()
}
func tryConvert(numStr string, defaultVal int) int {
+8 -4
View File
@@ -27,6 +27,7 @@ func TestIntegration(t *testing.T) {
parallelTotal := tryConvert(os.Getenv("PARALLEL_TOTAL"), 1)
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
raceDetector := os.Getenv("LAZYGIT_RACE_DETECTOR") != ""
testNumber := 0
err := components.RunTests(
@@ -52,6 +53,8 @@ func TestIntegration(t *testing.T) {
})
},
false,
false,
raceDetector,
0,
// Allow two attempts at each test to get around flakiness
2,
@@ -60,7 +63,7 @@ func TestIntegration(t *testing.T) {
assert.NoError(t, err)
}
func runCmdHeadless(cmd *exec.Cmd) error {
func runCmdHeadless(cmd *exec.Cmd) (int, error) {
cmd.Env = append(
cmd.Env,
"HEADLESS=true",
@@ -78,15 +81,16 @@ func runCmdHeadless(cmd *exec.Cmd) error {
// running other commands in a pty.
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 300, Cols: 300})
if err != nil {
return err
return -1, err
}
_, _ = io.Copy(io.Discard, f)
if cmd.Wait() != nil {
_ = f.Close()
// return an error with the stderr output
return errors.New(stderr.String())
return cmd.Process.Pid, errors.New(stderr.String())
}
return f.Close()
return cmd.Process.Pid, f.Close()
}
+29
View File
@@ -3,12 +3,14 @@ package main
import (
"fmt"
"os"
"time"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/mitchellh/go-ps"
)
// The purpose of this program is to run lazygit with an integration test passed in.
@@ -29,6 +31,15 @@ func main() {
integrationTest := getIntegrationTest()
if os.Getenv("WAIT_FOR_DEBUGGER") != "" {
println("Waiting for debugger to attach...")
for !isDebuggerAttached() {
time.Sleep(time.Millisecond * 100)
}
println("Debugger attached, continuing")
}
app.Start(dummyBuildInfo, integrationTest)
}
@@ -56,3 +67,21 @@ func getIntegrationTest() integrationTypes.IntegrationTest {
panic("Could not find integration test with name: " + integrationTestName)
}
// Returns whether we are running under a debugger. It uses a heuristic to find
// out: when using dlv, it starts a debugserver executable (which is part of
// lldb), and the debuggee becomes a child process of that. So if the name of
// our parent process is "debugserver", we run under a debugger. This works even
// if the parent process used to be the shell and you then attach to the running
// executable.
//
// On Mac this works with VS Code, with the Jetbrains Goland IDE, and when using
// dlv attach in a terminal. I have not been able to verify that it works on
// other platforms, it may have to be adapted there.
func isDebuggerAttached() bool {
process, err := ps.FindProcess(os.Getppid())
if err != nil {
return false
}
return process.Executable() == "debugserver"
}
+24 -9
View File
@@ -21,7 +21,7 @@ import (
var SLOW_INPUT_DELAY = 600
func RunTUI() {
func RunTUI(raceDetector bool) {
rootDir := utils.GetLazyRootDirectory()
testDir := filepath.Join(rootDir, "test", "integration")
@@ -85,7 +85,7 @@ func RunTUI() {
return nil
}
suspendAndRunTest(currentTest, true, 0)
suspendAndRunTest(currentTest, true, false, raceDetector, 0)
return nil
}); err != nil {
@@ -98,7 +98,7 @@ func RunTUI() {
return nil
}
suspendAndRunTest(currentTest, false, 0)
suspendAndRunTest(currentTest, false, false, raceDetector, 0)
return nil
}); err != nil {
@@ -111,7 +111,20 @@ func RunTUI() {
return nil
}
suspendAndRunTest(currentTest, false, SLOW_INPUT_DELAY)
suspendAndRunTest(currentTest, false, false, raceDetector, SLOW_INPUT_DELAY)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'd', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
suspendAndRunTest(currentTest, false, true, raceDetector, 0)
return nil
}); err != nil {
@@ -140,7 +153,7 @@ func RunTUI() {
return nil
}
cmd := exec.Command("sh", "-c", fmt.Sprintf("code test/results/%s", currentTest.Name()))
cmd := exec.Command("sh", "-c", fmt.Sprintf("code test/_results/%s", currentTest.Name()))
if err := cmd.Run(); err != nil {
return err
}
@@ -271,12 +284,12 @@ func (self *app) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod go
}
}
func suspendAndRunTest(test *components.IntegrationTest, sandbox bool, inputDelay int) {
func suspendAndRunTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
if err := gocui.Screen.Suspend(); err != nil {
panic(err)
}
runTuiTest(test, sandbox, inputDelay)
runTuiTest(test, sandbox, waitForDebugger, raceDetector, inputDelay)
fmt.Fprintf(os.Stdout, "\n%s", style.FgGreen.Sprint("press enter to return"))
fmt.Scanln() // wait for enter press
@@ -337,7 +350,7 @@ func (self *app) layout(g *gocui.Gui) error {
keybindingsView.Title = "Keybindings"
keybindingsView.Wrap = true
keybindingsView.FgColor = gocui.ColorDefault
fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, t: run test slow, s: sandbox, o: open test file, shift+o: open test snapshot directory, forward-slash: filter")
fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, t: run test slow, s: sandbox, d: debug test, o: open test file, shift+o: open test snapshot directory, forward-slash: filter")
}
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
@@ -371,13 +384,15 @@ func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
func runTuiTest(test *components.IntegrationTest, sandbox bool, inputDelay int) {
func runTuiTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
err := components.RunTests(
[]*components.IntegrationTest{test},
log.Printf,
runCmdInTerminal,
runAndPrintError,
sandbox,
waitForDebugger,
raceDetector,
inputDelay,
1,
)
+2 -2
View File
@@ -3,9 +3,9 @@ package components
import "path/filepath"
// convenience struct for easily getting directories within our test directory.
// We have one test directory for each test, found in test/results.
// We have one test directory for each test, found in test/_results.
type Paths struct {
// e.g. test/results/test_name
// e.g. test/_results/test_name
root string
}
+37 -15
View File
@@ -26,9 +26,11 @@ const (
func RunTests(
tests []*IntegrationTest,
logf func(format string, formatArgs ...interface{}),
runCmd func(cmd *exec.Cmd) error,
runCmd func(cmd *exec.Cmd) (int, error),
testWrapper func(test *IntegrationTest, f func() error),
sandbox bool,
waitForDebugger bool,
raceDetector bool,
inputDelay int,
maxAttempts int,
) error {
@@ -38,9 +40,9 @@ func RunTests(
return err
}
testDir := filepath.Join(projectRootDir, "test", "results")
testDir := filepath.Join(projectRootDir, "test", "_results")
if err := buildLazygit(); err != nil {
if err := buildLazygit(raceDetector); err != nil {
return err
}
@@ -58,7 +60,7 @@ func RunTests(
)
for i := 0; i < maxAttempts; i++ {
err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, inputDelay, gitVersion)
err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, waitForDebugger, raceDetector, inputDelay, gitVersion)
if err != nil {
if i == maxAttempts-1 {
return err
@@ -81,8 +83,10 @@ func runTest(
paths Paths,
projectRootDir string,
logf func(format string, formatArgs ...interface{}),
runCmd func(cmd *exec.Cmd) error,
runCmd func(cmd *exec.Cmd) (int, error),
sandbox bool,
waitForDebugger bool,
raceDetector bool,
inputDelay int,
gitVersion *git_commands.GitVersion,
) error {
@@ -100,17 +104,22 @@ func runTest(
return err
}
cmd, err := getLazygitCommand(test, paths, projectRootDir, sandbox, inputDelay)
cmd, err := getLazygitCommand(test, paths, projectRootDir, sandbox, waitForDebugger, inputDelay)
if err != nil {
return err
}
err = runCmd(cmd)
if err != nil {
return err
pid, err := runCmd(cmd)
// Print race detector log regardless of the command's exit status
if raceDetector {
logPath := fmt.Sprintf("%s.%d", raceDetectorLogsPath(), pid)
if bytes, err := os.ReadFile(logPath); err == nil {
logf("Race detector log:\n" + string(bytes))
}
}
return nil
return err
}
func prepareTestDir(
@@ -129,15 +138,18 @@ func prepareTestDir(
return createFixture(test, paths, rootDir)
}
func buildLazygit() error {
func buildLazygit(raceDetector bool) error {
// // TODO: remove this line!
// // skipping this because I'm not making changes to the app code atm.
// return nil
args := []string{"go", "build"}
if raceDetector {
args = append(args, "-race")
}
args = append(args, "-o", tempLazygitPath(), filepath.FromSlash("pkg/integration/clients/injector/main.go"))
osCommand := oscommands.NewDummyOSCommand()
return osCommand.Cmd.New([]string{
"go", "build", "-o", tempLazygitPath(), filepath.FromSlash("pkg/integration/clients/injector/main.go"),
}).Run()
return osCommand.Cmd.New(args).Run()
}
func createFixture(test *IntegrationTest, paths Paths, rootDir string) error {
@@ -165,7 +177,7 @@ func getGitVersion() (*git_commands.GitVersion, error) {
return git_commands.ParseGitVersion(versionStr)
}
func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandbox bool, inputDelay int) (*exec.Cmd, error) {
func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandbox bool, waitForDebugger bool, inputDelay int) (*exec.Cmd, error) {
osCommand := oscommands.NewDummyOSCommand()
err := os.RemoveAll(paths.Config())
@@ -197,6 +209,12 @@ func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandb
if sandbox {
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", SANDBOX_ENV_VAR, "true"))
}
if waitForDebugger {
cmdObj.AddEnvVars("WAIT_FOR_DEBUGGER=true")
}
// Set a race detector log path only to avoid spamming the terminal with the
// logs. We are not showing this anywhere yet.
cmdObj.AddEnvVars(fmt.Sprintf("GORACE=log_path=%s", raceDetectorLogsPath()))
if test.ExtraEnvVars() != nil {
for key, value := range test.ExtraEnvVars() {
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", key, value))
@@ -216,6 +234,10 @@ func tempLazygitPath() string {
return filepath.Join("/tmp", "lazygit", "test_lazygit")
}
func raceDetectorLogsPath() string {
return filepath.Join("/tmp", "lazygit", "race_log")
}
func findOrCreateDir(path string) {
_, err := os.Stat(path)
if err != nil {
@@ -0,0 +1,87 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Delete a remote branch where credentials are required",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("one")
shell.CloneIntoRemote("origin")
shell.NewBranch("mybranch")
shell.PushBranch("origin", "mybranch")
// actually getting a password prompt is tricky: it requires SSH'ing into localhost under a newly created, restricted, user.
// This is not easy to do in a cross-platform way, nor is it easy to do in a docker container.
// If you can think of a way to do it, please let me know!
shell.CopyHelpFile("pre-push", ".git/hooks/pre-push")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
deleteBranch := func() {
t.Views().Branches().
Focus().
Press(keys.Universal.Remove)
t.ExpectPopup().
Menu().
Title(Equals("Delete branch 'mybranch'?")).
Select(Contains("Delete remote branch")).
Confirm()
t.ExpectPopup().
Confirmation().
Title(Equals("Delete branch 'mybranch'?")).
Content(Equals("Are you sure you want to delete the remote branch 'mybranch' from 'origin'?")).
Confirm()
}
t.Views().Status().Content(Contains("✓ repo → mybranch"))
deleteBranch()
// correct credentials are: username=username, password=password
t.ExpectPopup().Prompt().
Title(Equals("Username")).
Type("username").
Confirm()
// enter incorrect password
t.ExpectPopup().Prompt().
Title(Equals("Password")).
Type("incorrect password").
Confirm()
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Contains("incorrect username/password")).
Confirm()
t.Views().Status().Content(Contains("✓ repo → mybranch"))
// try again with correct password
deleteBranch()
t.ExpectPopup().Prompt().
Title(Equals("Username")).
Type("username").
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Password")).
Type("password").
Confirm()
t.Views().Status().Content(Contains("repo → mybranch").DoesNotContain("✓"))
t.Views().Branches().TopLines(Contains("mybranch (upstream gone)"))
},
})
@@ -0,0 +1,25 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var OpenPullRequestNoUpstream = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open up a pull request with a missing upstream branch",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().
Branches().
Focus().
Press(keys.Branches.CreatePullRequest)
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Contains("Cannot open a pull request for a branch with no upstream")).
Confirm()
},
})
@@ -0,0 +1,82 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RebaseToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Rebase the current branch to the selected branch upstream",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.
CloneIntoRemote("origin").
EmptyCommit("ensure-master").
EmptyCommit("to-be-added"). // <- this will only exist remotely
PushBranch("origin", "master").
HardReset("HEAD~1").
NewBranchFrom("base-branch", "master").
EmptyCommit("base-branch-commit").
NewBranch("target").
EmptyCommit("target-commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().Lines(
Contains("target-commit"),
Contains("base-branch-commit"),
Contains("ensure-master"),
)
t.Views().Branches().
Focus().
Lines(
Contains("target").IsSelected(),
Contains("base-branch"),
Contains("master"),
).
SelectNextItem().
Lines(
Contains("target"),
Contains("base-branch").IsSelected(),
Contains("master"),
).
Press(keys.Branches.SetUpstream).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Upstream options")).
Select(Contains("Rebase checked-out branch onto upstream of selected branch")).
Tooltip(Contains("Disabled: The selected branch has no upstream (or the upstream is not stored locally)")).
Confirm()
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Equals("The selected branch has no upstream (or the upstream is not stored locally)")).
Confirm()
}).
SelectNextItem().
Lines(
Contains("target"),
Contains("base-branch"),
Contains("master").IsSelected(),
).
Press(keys.Branches.SetUpstream).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Upstream options")).
Select(Contains("Rebase checked-out branch onto origin/master...")).
Confirm()
t.ExpectPopup().Menu().
Title(Equals("Rebase 'target' onto 'origin/master'")).
Select(Contains("Simple rebase")).
Confirm()
})
t.Views().Commits().Lines(
Contains("target-commit"),
Contains("base-branch-commit"),
Contains("to-be-added"),
Contains("ensure-master"),
)
},
})
@@ -41,11 +41,11 @@ var ResetToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
t.ExpectPopup().Menu().
Title(Equals("Upstream options")).
Select(Contains("Reset checked-out branch onto upstream of selected branch")).
Tooltip(Contains("The selected branch has no upstream")).
Tooltip(Contains("Disabled: The selected branch has no upstream (or the upstream is not stored locally)")).
Confirm()
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Equals("The selected branch has no upstream")).
Content(Equals("The selected branch has no upstream (or the upstream is not stored locally)")).
Confirm()
}).
SelectNextItem().
@@ -0,0 +1,87 @@
package cherry_pick
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CherryPickDuringRebase = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Cherry pick commits from the subcommits view during a rebase",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.
EmptyCommit("base").
NewBranch("first-branch").
NewBranch("second-branch").
Checkout("first-branch").
EmptyCommit("one").
EmptyCommit("two").
Checkout("second-branch").
EmptyCommit("three").
EmptyCommit("four").
Checkout("first-branch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("first-branch"),
Contains("second-branch"),
Contains("master"),
).
SelectNextItem().
PressEnter()
t.Views().SubCommits().
IsFocused().
Lines(
Contains("four").IsSelected(),
Contains("three"),
Contains("base"),
).
// copy commit 'three'
SelectNextItem().
Press(keys.Commits.CherryPickCopy)
t.Views().Information().Content(Contains("1 commit copied"))
t.Views().Commits().
Focus().
Lines(
Contains("CI two").IsSelected(),
Contains("CI one"),
Contains("CI base"),
).
SelectNextItem().
Press(keys.Universal.Edit).
Lines(
Contains("pick CI two"),
Contains(" CI <-- YOU ARE HERE --- one").IsSelected(),
Contains(" CI base"),
).
Press(keys.Commits.PasteCommits).
Tap(func() {
t.ExpectPopup().Alert().
Title(Equals("Cherry-pick")).
Content(Contains("Are you sure you want to cherry-pick the copied commits onto this branch?")).
Confirm()
}).
Lines(
Contains("pick CI two"),
Contains("pick CI three"),
Contains(" CI <-- YOU ARE HERE --- one"),
Contains(" CI base"),
).
Tap(func() {
t.Common().ContinueRebase()
}).
Lines(
Contains("CI two"),
Contains("CI three"),
Contains("CI one"),
Contains("CI base"),
)
},
})
@@ -0,0 +1,77 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterSearchHistory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Navigating search history",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
// populate search history with some values
FilterOrSearch("1").
FilterOrSearch("2").
FilterOrSearch("3").
Press(keys.Universal.StartSearch).
// clear initial search value
Tap(func() {
t.ExpectSearch().Clear()
}).
// test main search history functionality
Tap(func() {
t.Views().Search().
Press(keys.Universal.PrevItem).
Content(Contains("3")).
Press(keys.Universal.PrevItem).
Content(Contains("2")).
Press(keys.Universal.PrevItem).
Content(Contains("1")).
Press(keys.Universal.PrevItem).
Content(Contains("1")).
Press(keys.Universal.NextItem).
Content(Contains("2")).
Press(keys.Universal.NextItem).
Content(Contains("3")).
Press(keys.Universal.NextItem).
Content(Contains("")).
Press(keys.Universal.NextItem).
Content(Contains("")).
Press(keys.Universal.PrevItem).
Content(Contains("3")).
PressEscape()
}).
// test that it resets after you enter and exit a search
Press(keys.Universal.StartSearch).
Tap(func() {
t.Views().Search().
Press(keys.Universal.PrevItem).
Content(Contains("3")).
PressEscape()
})
// test that the histories are separate for each view
t.Views().Commits().
Focus().
FilterOrSearch("a").
FilterOrSearch("b").
FilterOrSearch("c").
Press(keys.Universal.StartSearch).
Tap(func() {
t.ExpectSearch().Clear()
}).
Tap(func() {
t.Views().Search().
Press(keys.Universal.PrevItem).
Content(Contains("c")).
Press(keys.Universal.PrevItem).
Content(Contains("b")).
Press(keys.Universal.PrevItem).
Content(Contains("a"))
})
},
})
@@ -0,0 +1,39 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// This is a regression test to ensure https://github.com/jesseduffield/lazygit/issues/2971
// doesn't happen again
var NewSearch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Start a new search and verify the search begins from the current cursor position, not from the current search match",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
// need to create some branches, each with their own commits
shell.EmptyCommit("Add foo")
shell.EmptyCommit("Remove foo")
shell.EmptyCommit("Add bar")
shell.EmptyCommit("Remove bar")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains(`Remove bar`).IsSelected(),
Contains(`Add bar`),
Contains(`Remove foo`),
Contains(`Add foo`),
).
FilterOrSearch("Add").
SelectedLine(Contains(`Add bar`)).
SelectPreviousItem().
SelectedLine(Contains(`Remove bar`)).
FilterOrSearch("Remove").
SelectedLine(Contains(`Remove bar`))
},
})
+6
View File
@@ -38,7 +38,9 @@ var tests = []*components.IntegrationTest{
branch.CheckoutByName,
branch.CreateTag,
branch.Delete,
branch.DeleteRemoteBranchWithCredentialPrompt,
branch.DetachedHead,
branch.OpenPullRequestNoUpstream,
branch.OpenWithCliArg,
branch.Rebase,
branch.RebaseAbortOnConflict,
@@ -46,6 +48,7 @@ var tests = []*components.IntegrationTest{
branch.RebaseCancelOnConflict,
branch.RebaseDoesNotAutosquash,
branch.RebaseFromMarkedBase,
branch.RebaseToUpstream,
branch.Rename,
branch.Reset,
branch.ResetToUpstream,
@@ -55,6 +58,7 @@ var tests = []*components.IntegrationTest{
branch.Suggestions,
cherry_pick.CherryPick,
cherry_pick.CherryPickConflicts,
cherry_pick.CherryPickDuringRebase,
commit.AddCoAuthor,
commit.Amend,
commit.Commit,
@@ -127,8 +131,10 @@ var tests = []*components.IntegrationTest{
filter_and_search.FilterMenu,
filter_and_search.FilterMenuCancelFilterWithEscape,
filter_and_search.FilterRemoteBranches,
filter_and_search.FilterSearchHistory,
filter_and_search.NestedFilter,
filter_and_search.NestedFilterTransient,
filter_and_search.NewSearch,
filter_by_path.CliArg,
filter_by_path.SelectFile,
filter_by_path.TypeFile,
+36
View File
@@ -0,0 +1,36 @@
package utils
import "fmt"
type HistoryBuffer[T any] struct {
maxSize int
items []T
}
func NewHistoryBuffer[T any](maxSize int) *HistoryBuffer[T] {
return &HistoryBuffer[T]{
maxSize: maxSize,
items: make([]T, 0, maxSize),
}
}
func (self *HistoryBuffer[T]) Push(item T) {
if len(self.items) == self.maxSize {
self.items = self.items[:len(self.items)-1]
}
self.items = append([]T{item}, self.items...)
}
func (self *HistoryBuffer[T]) PeekAt(index int) (T, error) {
var item T
if len(self.items) == 0 {
return item, fmt.Errorf("Buffer is empty")
}
if len(self.items) <= index || index < -1 {
return item, fmt.Errorf("Index out of range")
}
if index == -1 {
return item, nil
}
return self.items[index], nil
}
+64
View File
@@ -0,0 +1,64 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewHistoryBuffer(t *testing.T) {
hb := NewHistoryBuffer[int](5)
assert.NotNil(t, hb)
assert.Equal(t, 5, hb.maxSize)
assert.Equal(t, 0, len(hb.items))
}
func TestPush(t *testing.T) {
hb := NewHistoryBuffer[int](3)
hb.Push(1)
hb.Push(2)
hb.Push(3)
hb.Push(4)
assert.Equal(t, 3, len(hb.items))
assert.Equal(t, []int{4, 3, 2}, hb.items)
}
func TestPeekAt(t *testing.T) {
hb := NewHistoryBuffer[int](3)
hb.Push(1)
hb.Push(2)
hb.Push(3)
item, err := hb.PeekAt(0)
assert.Nil(t, err)
assert.Equal(t, 3, item)
item, err = hb.PeekAt(1)
assert.Nil(t, err)
assert.Equal(t, 2, item)
item, err = hb.PeekAt(2)
assert.Nil(t, err)
assert.Equal(t, 1, item)
item, err = hb.PeekAt(-1)
assert.Nil(t, err)
assert.Equal(t, 0, item)
_, err = hb.PeekAt(3)
assert.NotNil(t, err)
assert.Equal(t, "Index out of range", err.Error())
_, err = hb.PeekAt(-2)
assert.NotNil(t, err)
assert.Equal(t, "Index out of range", err.Error())
}
func TestPeekAtEmptyBuffer(t *testing.T) {
hb := NewHistoryBuffer[int](3)
_, err := hb.PeekAt(0)
assert.NotNil(t, err)
assert.Equal(t, "Buffer is empty", err.Error())
}
+5 -2
View File
@@ -24,12 +24,15 @@ func GetProjectRoot() string {
return strings.Split(dir, "lazygit")[0] + "lazygit"
}
// The duration between two frames of the loader animation in milliseconds
const LoaderAnimationInterval = 50
// Loader dumps a string to be displayed as a loader
func Loader() string {
characters := "|/-\\"
now := time.Now()
nanos := now.UnixNano()
index := nanos / 50000000 % int64(len(characters))
milliseconds := now.UnixMilli()
index := milliseconds / LoaderAnimationInterval % int64(len(characters))
return characters[index : index+1]
}
+1
View File
@@ -81,3 +81,4 @@ This package is licensed under the MIT license, see LICENSE.MIT for details.
* v1.4.1 no code change, but now without an unnecessary cover.out file.
* v1.4.2 performance improvement to ErrorStack() to avoid unnecessary work https://github.com/go-errors/errors/pull/40
* v1.5.0 add errors.Join() and errors.Unwrap() copying the stdlib https://github.com/go-errors/errors/pull/40
* v1.5.1 fix build on go1.13..go1.19 (broken by adding Join and Unwrap with wrong build constraints)
-26
View File
@@ -33,29 +33,3 @@ func Is(e error, original error) bool {
return false
}
// Join returns an error that wraps the given errors.
// Any nil error values are discarded.
// Join returns nil if every value in errs is nil.
// The error formats as the concatenation of the strings obtained
// by calling the Error method of each element of errs, with a newline
// between each string.
//
// A non-nil error returned by Join implements the Unwrap() []error method.
//
// For more information see stdlib errors.Join.
func Join(errs ...error) error {
return baseErrors.Join(errs...)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
//
// Unwrap only calls a method of the form "Unwrap() error".
// In particular Unwrap does not unwrap errors returned by [Join].
//
// For more information see stdlib errors.Unwrap.
func Unwrap(err error) error {
return baseErrors.Unwrap(err)
}
+32
View File
@@ -0,0 +1,32 @@
//go:build go1.20
// +build go1.20
package errors
import baseErrors "errors"
// Join returns an error that wraps the given errors.
// Any nil error values are discarded.
// Join returns nil if every value in errs is nil.
// The error formats as the concatenation of the strings obtained
// by calling the Error method of each element of errs, with a newline
// between each string.
//
// A non-nil error returned by Join implements the Unwrap() []error method.
//
// For more information see stdlib errors.Join.
func Join(errs ...error) error {
return baseErrors.Join(errs...)
}
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
//
// Unwrap only calls a method of the form "Unwrap() error".
// In particular Unwrap does not unwrap errors returned by [Join].
//
// For more information see stdlib errors.Unwrap.
func Unwrap(err error) error {
return baseErrors.Unwrap(err)
}
+71
View File
@@ -0,0 +1,71 @@
//go:build !go1.20
// +build !go1.20
package errors
// Disclaimer: functions Join and Unwrap are copied from the stdlib errors
// package v1.21.0.
// Join returns an error that wraps the given errors.
// Any nil error values are discarded.
// Join returns nil if every value in errs is nil.
// The error formats as the concatenation of the strings obtained
// by calling the Error method of each element of errs, with a newline
// between each string.
//
// A non-nil error returned by Join implements the Unwrap() []error method.
func Join(errs ...error) error {
n := 0
for _, err := range errs {
if err != nil {
n++
}
}
if n == 0 {
return nil
}
e := &joinError{
errs: make([]error, 0, n),
}
for _, err := range errs {
if err != nil {
e.errs = append(e.errs, err)
}
}
return e
}
type joinError struct {
errs []error
}
func (e *joinError) Error() string {
var b []byte
for i, err := range e.errs {
if i > 0 {
b = append(b, '\n')
}
b = append(b, err.Error()...)
}
return string(b)
}
func (e *joinError) Unwrap() []error {
return e.errs
}
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
//
// Unwrap only calls a method of the form "Unwrap() error".
// In particular Unwrap does not unwrap errors returned by [Join].
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
+6 -4
View File
@@ -211,10 +211,6 @@ func (v *View) gotoPreviousMatch() error {
return v.SelectSearchResult(v.searcher.currentSearchIndex)
}
func (v *View) SelectCurrentSearchResult() error {
return v.SelectSearchResult(v.searcher.currentSearchIndex)
}
func (v *View) SelectSearchResult(index int) error {
itemCount := len(v.searcher.searchPositions)
if itemCount == 0 {
@@ -225,6 +221,7 @@ func (v *View) SelectSearchResult(index int) error {
}
y := v.searcher.searchPositions[index].y
v.FocusPoint(v.ox, y)
if v.searcher.onSelectItem != nil {
return v.searcher.onSelectItem(y, index, itemCount)
@@ -232,6 +229,11 @@ func (v *View) SelectSearchResult(index int) error {
return nil
}
// Returns <current match index>, <total matches>
func (v *View) GetSearchStatus() (int, int) {
return v.searcher.currentSearchIndex, len(v.searcher.searchPositions)
}
func (v *View) Search(str string) error {
v.writeMutex.Lock()
v.searcher.search(str)
+1
View File
@@ -0,0 +1 @@
.vagrant/
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+34
View File
@@ -0,0 +1,34 @@
# Process List Library for Go [![GoDoc](https://godoc.org/github.com/mitchellh/go-ps?status.png)](https://godoc.org/github.com/mitchellh/go-ps)
go-ps is a library for Go that implements OS-specific APIs to list and
manipulate processes in a platform-safe way. The library can find and
list processes on Linux, Mac OS X, Solaris, and Windows.
If you're new to Go, this library has a good amount of advanced Go educational
value as well. It uses some advanced features of Go: build tags, accessing
DLL methods for Windows, cgo for Darwin, etc.
How it works:
* **Darwin** uses the `sysctl` syscall to retrieve the process table.
* **Unix** uses the procfs at `/proc` to inspect the process tree.
* **Windows** uses the Windows API, and methods such as
`CreateToolhelp32Snapshot` to get a point-in-time snapshot of
the process table.
## Installation
Install using standard `go get`:
```
$ go get github.com/mitchellh/go-ps
...
```
## TODO
Want to contribute? Here is a short TODO list of things that aren't
implemented for this library that would be nice:
* FreeBSD support
* Plan9 support
+43
View File
@@ -0,0 +1,43 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "chef/ubuntu-12.04"
config.vm.provision "shell", inline: $script
["vmware_fusion", "vmware_workstation"].each do |p|
config.vm.provider "p" do |v|
v.vmx["memsize"] = "1024"
v.vmx["numvcpus"] = "2"
v.vmx["cpuid.coresPerSocket"] = "1"
end
end
end
$script = <<SCRIPT
SRCROOT="/opt/go"
# Install Go
sudo apt-get update
sudo apt-get install -y build-essential mercurial
sudo hg clone -u release https://code.google.com/p/go ${SRCROOT}
cd ${SRCROOT}/src
sudo ./all.bash
# Setup the GOPATH
sudo mkdir -p /opt/gopath
cat <<EOF >/tmp/gopath.sh
export GOPATH="/opt/gopath"
export PATH="/opt/go/bin:\$GOPATH/bin:\$PATH"
EOF
sudo mv /tmp/gopath.sh /etc/profile.d/gopath.sh
sudo chmod 0755 /etc/profile.d/gopath.sh
# Make sure the gopath is usable by bamboo
sudo chown -R vagrant:vagrant $SRCROOT
sudo chown -R vagrant:vagrant /opt/gopath
SCRIPT
+40
View File
@@ -0,0 +1,40 @@
// ps provides an API for finding and listing processes in a platform-agnostic
// way.
//
// NOTE: If you're reading these docs online via GoDocs or some other system,
// you might only see the Unix docs. This project makes heavy use of
// platform-specific implementations. We recommend reading the source if you
// are interested.
package ps
// Process is the generic interface that is implemented on every platform
// and provides common operations for processes.
type Process interface {
// Pid is the process ID for this process.
Pid() int
// PPid is the parent process ID for this process.
PPid() int
// Executable name running this process. This is not a path to the
// executable.
Executable() string
}
// Processes returns all processes.
//
// This of course will be a point-in-time snapshot of when this method was
// called. Some operating systems don't provide snapshot capability of the
// process table, in which case the process table returned might contain
// ephemeral entities that happened to be running when this was called.
func Processes() ([]Process, error) {
return processes()
}
// FindProcess looks up a single process by pid.
//
// Process will be nil and error will be nil if a matching process is
// not found.
func FindProcess(pid int) (Process, error) {
return findProcess(pid)
}
+138
View File
@@ -0,0 +1,138 @@
// +build darwin
package ps
import (
"bytes"
"encoding/binary"
"syscall"
"unsafe"
)
type DarwinProcess struct {
pid int
ppid int
binary string
}
func (p *DarwinProcess) Pid() int {
return p.pid
}
func (p *DarwinProcess) PPid() int {
return p.ppid
}
func (p *DarwinProcess) Executable() string {
return p.binary
}
func findProcess(pid int) (Process, error) {
ps, err := processes()
if err != nil {
return nil, err
}
for _, p := range ps {
if p.Pid() == pid {
return p, nil
}
}
return nil, nil
}
func processes() ([]Process, error) {
buf, err := darwinSyscall()
if err != nil {
return nil, err
}
procs := make([]*kinfoProc, 0, 50)
k := 0
for i := _KINFO_STRUCT_SIZE; i < buf.Len(); i += _KINFO_STRUCT_SIZE {
proc := &kinfoProc{}
err = binary.Read(bytes.NewBuffer(buf.Bytes()[k:i]), binary.LittleEndian, proc)
if err != nil {
return nil, err
}
k = i
procs = append(procs, proc)
}
darwinProcs := make([]Process, len(procs))
for i, p := range procs {
darwinProcs[i] = &DarwinProcess{
pid: int(p.Pid),
ppid: int(p.PPid),
binary: darwinCstring(p.Comm),
}
}
return darwinProcs, nil
}
func darwinCstring(s [16]byte) string {
i := 0
for _, b := range s {
if b != 0 {
i++
} else {
break
}
}
return string(s[:i])
}
func darwinSyscall() (*bytes.Buffer, error) {
mib := [4]int32{_CTRL_KERN, _KERN_PROC, _KERN_PROC_ALL, 0}
size := uintptr(0)
_, _, errno := syscall.Syscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
4,
0,
uintptr(unsafe.Pointer(&size)),
0,
0)
if errno != 0 {
return nil, errno
}
bs := make([]byte, size)
_, _, errno = syscall.Syscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
4,
uintptr(unsafe.Pointer(&bs[0])),
uintptr(unsafe.Pointer(&size)),
0,
0)
if errno != 0 {
return nil, errno
}
return bytes.NewBuffer(bs[0:size]), nil
}
const (
_CTRL_KERN = 1
_KERN_PROC = 14
_KERN_PROC_ALL = 0
_KINFO_STRUCT_SIZE = 648
)
type kinfoProc struct {
_ [40]byte
Pid int32
_ [199]byte
Comm [16]byte
_ [301]byte
PPid int32
_ [84]byte
}
+260
View File
@@ -0,0 +1,260 @@
// +build freebsd
package ps
import (
"bytes"
"encoding/binary"
"syscall"
"unsafe"
)
// copied from sys/sysctl.h
const (
CTL_KERN = 1 // "high kernel": proc, limits
KERN_PROC = 14 // struct: process entries
KERN_PROC_PID = 1 // by process id
KERN_PROC_PROC = 8 // only return procs
KERN_PROC_PATHNAME = 12 // path to executable
)
// copied from sys/user.h
type Kinfo_proc struct {
Ki_structsize int32
Ki_layout int32
Ki_args int64
Ki_paddr int64
Ki_addr int64
Ki_tracep int64
Ki_textvp int64
Ki_fd int64
Ki_vmspace int64
Ki_wchan int64
Ki_pid int32
Ki_ppid int32
Ki_pgid int32
Ki_tpgid int32
Ki_sid int32
Ki_tsid int32
Ki_jobc [2]byte
Ki_spare_short1 [2]byte
Ki_tdev int32
Ki_siglist [16]byte
Ki_sigmask [16]byte
Ki_sigignore [16]byte
Ki_sigcatch [16]byte
Ki_uid int32
Ki_ruid int32
Ki_svuid int32
Ki_rgid int32
Ki_svgid int32
Ki_ngroups [2]byte
Ki_spare_short2 [2]byte
Ki_groups [64]byte
Ki_size int64
Ki_rssize int64
Ki_swrss int64
Ki_tsize int64
Ki_dsize int64
Ki_ssize int64
Ki_xstat [2]byte
Ki_acflag [2]byte
Ki_pctcpu int32
Ki_estcpu int32
Ki_slptime int32
Ki_swtime int32
Ki_cow int32
Ki_runtime int64
Ki_start [16]byte
Ki_childtime [16]byte
Ki_flag int64
Ki_kiflag int64
Ki_traceflag int32
Ki_stat [1]byte
Ki_nice [1]byte
Ki_lock [1]byte
Ki_rqindex [1]byte
Ki_oncpu [1]byte
Ki_lastcpu [1]byte
Ki_ocomm [17]byte
Ki_wmesg [9]byte
Ki_login [18]byte
Ki_lockname [9]byte
Ki_comm [20]byte
Ki_emul [17]byte
Ki_sparestrings [68]byte
Ki_spareints [36]byte
Ki_cr_flags int32
Ki_jid int32
Ki_numthreads int32
Ki_tid int32
Ki_pri int32
Ki_rusage [144]byte
Ki_rusage_ch [144]byte
Ki_pcb int64
Ki_kstack int64
Ki_udata int64
Ki_tdaddr int64
Ki_spareptrs [48]byte
Ki_spareint64s [96]byte
Ki_sflag int64
Ki_tdflags int64
}
// UnixProcess is an implementation of Process that contains Unix-specific
// fields and information.
type UnixProcess struct {
pid int
ppid int
state rune
pgrp int
sid int
binary string
}
func (p *UnixProcess) Pid() int {
return p.pid
}
func (p *UnixProcess) PPid() int {
return p.ppid
}
func (p *UnixProcess) Executable() string {
return p.binary
}
// Refresh reloads all the data associated with this process.
func (p *UnixProcess) Refresh() error {
mib := []int32{CTL_KERN, KERN_PROC, KERN_PROC_PID, int32(p.pid)}
buf, length, err := call_syscall(mib)
if err != nil {
return err
}
proc_k := Kinfo_proc{}
if length != uint64(unsafe.Sizeof(proc_k)) {
return err
}
k, err := parse_kinfo_proc(buf)
if err != nil {
return err
}
p.ppid, p.pgrp, p.sid, p.binary = copy_params(&k)
return nil
}
func copy_params(k *Kinfo_proc) (int, int, int, string) {
n := -1
for i, b := range k.Ki_comm {
if b == 0 {
break
}
n = i + 1
}
comm := string(k.Ki_comm[:n])
return int(k.Ki_ppid), int(k.Ki_pgid), int(k.Ki_sid), comm
}
func findProcess(pid int) (Process, error) {
mib := []int32{CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, int32(pid)}
_, _, err := call_syscall(mib)
if err != nil {
return nil, err
}
return newUnixProcess(pid)
}
func processes() ([]Process, error) {
results := make([]Process, 0, 50)
mib := []int32{CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0}
buf, length, err := call_syscall(mib)
if err != nil {
return results, err
}
// get kinfo_proc size
k := Kinfo_proc{}
procinfo_len := int(unsafe.Sizeof(k))
count := int(length / uint64(procinfo_len))
// parse buf to procs
for i := 0; i < count; i++ {
b := buf[i*procinfo_len : i*procinfo_len+procinfo_len]
k, err := parse_kinfo_proc(b)
if err != nil {
continue
}
p, err := newUnixProcess(int(k.Ki_pid))
if err != nil {
continue
}
p.ppid, p.pgrp, p.sid, p.binary = copy_params(&k)
results = append(results, p)
}
return results, nil
}
func parse_kinfo_proc(buf []byte) (Kinfo_proc, error) {
var k Kinfo_proc
br := bytes.NewReader(buf)
err := binary.Read(br, binary.LittleEndian, &k)
if err != nil {
return k, err
}
return k, nil
}
func call_syscall(mib []int32) ([]byte, uint64, error) {
miblen := uint64(len(mib))
// get required buffer size
length := uint64(0)
_, _, err := syscall.RawSyscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
uintptr(miblen),
0,
uintptr(unsafe.Pointer(&length)),
0,
0)
if err != 0 {
b := make([]byte, 0)
return b, length, err
}
if length == 0 {
b := make([]byte, 0)
return b, length, err
}
// get proc info itself
buf := make([]byte, length)
_, _, err = syscall.RawSyscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
uintptr(miblen),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&length)),
0,
0)
if err != 0 {
return buf, length, err
}
return buf, length, nil
}
func newUnixProcess(pid int) (*UnixProcess, error) {
p := &UnixProcess{pid: pid}
return p, p.Refresh()
}
+35
View File
@@ -0,0 +1,35 @@
// +build linux
package ps
import (
"fmt"
"io/ioutil"
"strings"
)
// Refresh reloads all the data associated with this process.
func (p *UnixProcess) Refresh() error {
statPath := fmt.Sprintf("/proc/%d/stat", p.pid)
dataBytes, err := ioutil.ReadFile(statPath)
if err != nil {
return err
}
// First, parse out the image name
data := string(dataBytes)
binStart := strings.IndexRune(data, '(') + 1
binEnd := strings.IndexRune(data[binStart:], ')')
p.binary = data[binStart : binStart+binEnd]
// Move past the image name and start parsing the rest
data = data[binStart+binEnd+2:]
_, err = fmt.Sscanf(data,
"%c %d %d %d",
&p.state,
&p.ppid,
&p.pgrp,
&p.sid)
return err
}
+96
View File
@@ -0,0 +1,96 @@
// +build solaris
package ps
import (
"encoding/binary"
"fmt"
"os"
)
type ushort_t uint16
type id_t int32
type pid_t int32
type uid_t int32
type gid_t int32
type dev_t uint64
type size_t uint64
type uintptr_t uint64
type timestruc_t [16]byte
// This is copy from /usr/include/sys/procfs.h
type psinfo_t struct {
Pr_flag int32 /* process flags (DEPRECATED; do not use) */
Pr_nlwp int32 /* number of active lwps in the process */
Pr_pid pid_t /* unique process id */
Pr_ppid pid_t /* process id of parent */
Pr_pgid pid_t /* pid of process group leader */
Pr_sid pid_t /* session id */
Pr_uid uid_t /* real user id */
Pr_euid uid_t /* effective user id */
Pr_gid gid_t /* real group id */
Pr_egid gid_t /* effective group id */
Pr_addr uintptr_t /* address of process */
Pr_size size_t /* size of process image in Kbytes */
Pr_rssize size_t /* resident set size in Kbytes */
Pr_pad1 size_t
Pr_ttydev dev_t /* controlling tty device (or PRNODEV) */
// Guess this following 2 ushort_t values require a padding to properly
// align to the 64bit mark.
Pr_pctcpu ushort_t /* % of recent cpu time used by all lwps */
Pr_pctmem ushort_t /* % of system memory used by process */
Pr_pad64bit [4]byte
Pr_start timestruc_t /* process start time, from the epoch */
Pr_time timestruc_t /* usr+sys cpu time for this process */
Pr_ctime timestruc_t /* usr+sys cpu time for reaped children */
Pr_fname [16]byte /* name of execed file */
Pr_psargs [80]byte /* initial characters of arg list */
Pr_wstat int32 /* if zombie, the wait() status */
Pr_argc int32 /* initial argument count */
Pr_argv uintptr_t /* address of initial argument vector */
Pr_envp uintptr_t /* address of initial environment vector */
Pr_dmodel [1]byte /* data model of the process */
Pr_pad2 [3]byte
Pr_taskid id_t /* task id */
Pr_projid id_t /* project id */
Pr_nzomb int32 /* number of zombie lwps in the process */
Pr_poolid id_t /* pool id */
Pr_zoneid id_t /* zone id */
Pr_contract id_t /* process contract */
Pr_filler int32 /* reserved for future use */
Pr_lwp [128]byte /* information for representative lwp */
}
func (p *UnixProcess) Refresh() error {
var psinfo psinfo_t
path := fmt.Sprintf("/proc/%d/psinfo", p.pid)
fh, err := os.Open(path)
if err != nil {
return err
}
defer fh.Close()
err = binary.Read(fh, binary.LittleEndian, &psinfo)
if err != nil {
return err
}
p.ppid = int(psinfo.Pr_ppid)
p.binary = toString(psinfo.Pr_fname[:], 16)
return nil
}
func toString(array []byte, len int) string {
for i := 0; i < len; i++ {
if array[i] == 0 {
return string(array[:i])
}
}
return string(array[:])
}
+95
View File
@@ -0,0 +1,95 @@
// +build linux solaris
package ps
import (
"fmt"
"io"
"os"
"strconv"
)
// UnixProcess is an implementation of Process that contains Unix-specific
// fields and information.
type UnixProcess struct {
pid int
ppid int
state rune
pgrp int
sid int
binary string
}
func (p *UnixProcess) Pid() int {
return p.pid
}
func (p *UnixProcess) PPid() int {
return p.ppid
}
func (p *UnixProcess) Executable() string {
return p.binary
}
func findProcess(pid int) (Process, error) {
dir := fmt.Sprintf("/proc/%d", pid)
_, err := os.Stat(dir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
return newUnixProcess(pid)
}
func processes() ([]Process, error) {
d, err := os.Open("/proc")
if err != nil {
return nil, err
}
defer d.Close()
results := make([]Process, 0, 50)
for {
names, err := d.Readdirnames(10)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
for _, name := range names {
// We only care if the name starts with a numeric
if name[0] < '0' || name[0] > '9' {
continue
}
// From this point forward, any errors we just ignore, because
// it might simply be that the process doesn't exist anymore.
pid, err := strconv.ParseInt(name, 10, 0)
if err != nil {
continue
}
p, err := newUnixProcess(int(pid))
if err != nil {
continue
}
results = append(results, p)
}
}
return results, nil
}
func newUnixProcess(pid int) (*UnixProcess, error) {
p := &UnixProcess{pid: pid}
return p, p.Refresh()
}
+119
View File
@@ -0,0 +1,119 @@
// +build windows
package ps
import (
"fmt"
"syscall"
"unsafe"
)
// Windows API functions
var (
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
procCloseHandle = modKernel32.NewProc("CloseHandle")
procCreateToolhelp32Snapshot = modKernel32.NewProc("CreateToolhelp32Snapshot")
procProcess32First = modKernel32.NewProc("Process32FirstW")
procProcess32Next = modKernel32.NewProc("Process32NextW")
)
// Some constants from the Windows API
const (
ERROR_NO_MORE_FILES = 0x12
MAX_PATH = 260
)
// PROCESSENTRY32 is the Windows API structure that contains a process's
// information.
type PROCESSENTRY32 struct {
Size uint32
CntUsage uint32
ProcessID uint32
DefaultHeapID uintptr
ModuleID uint32
CntThreads uint32
ParentProcessID uint32
PriorityClassBase int32
Flags uint32
ExeFile [MAX_PATH]uint16
}
// WindowsProcess is an implementation of Process for Windows.
type WindowsProcess struct {
pid int
ppid int
exe string
}
func (p *WindowsProcess) Pid() int {
return p.pid
}
func (p *WindowsProcess) PPid() int {
return p.ppid
}
func (p *WindowsProcess) Executable() string {
return p.exe
}
func newWindowsProcess(e *PROCESSENTRY32) *WindowsProcess {
// Find when the string ends for decoding
end := 0
for {
if e.ExeFile[end] == 0 {
break
}
end++
}
return &WindowsProcess{
pid: int(e.ProcessID),
ppid: int(e.ParentProcessID),
exe: syscall.UTF16ToString(e.ExeFile[:end]),
}
}
func findProcess(pid int) (Process, error) {
ps, err := processes()
if err != nil {
return nil, err
}
for _, p := range ps {
if p.Pid() == pid {
return p, nil
}
}
return nil, nil
}
func processes() ([]Process, error) {
handle, _, _ := procCreateToolhelp32Snapshot.Call(
0x00000002,
0)
if handle < 0 {
return nil, syscall.GetLastError()
}
defer procCloseHandle.Call(handle)
var entry PROCESSENTRY32
entry.Size = uint32(unsafe.Sizeof(entry))
ret, _, _ := procProcess32First.Call(handle, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
return nil, fmt.Errorf("Error retrieving process info.")
}
results := make([]Process, 0, 50)
for {
results = append(results, newWindowsProcess(&entry))
ret, _, _ := procProcess32Next.Call(handle, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
break
}
}
return results, nil
}
+5 -2
View File
@@ -33,7 +33,7 @@ github.com/fsmiamoto/git-todo-parser/todo
# github.com/gdamore/encoding v1.0.0
## explicit; go 1.9
github.com/gdamore/encoding
# github.com/go-errors/errors v1.5.0
# github.com/go-errors/errors v1.5.1
## explicit; go 1.14
github.com/go-errors/errors
# github.com/go-git/gcfg v1.5.0
@@ -124,7 +124,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.20230909074155-fc7119a39341
# github.com/jesseduffield/gocui v0.3.1-0.20230925062444-7cd0d7e2a70a
## explicit; go 1.12
github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
@@ -175,6 +175,9 @@ github.com/mgutz/str
# github.com/mitchellh/go-homedir v1.1.0
## explicit
github.com/mitchellh/go-homedir
# github.com/mitchellh/go-ps v1.0.0
## explicit; go 1.13
github.com/mitchellh/go-ps
# github.com/onsi/ginkgo v1.10.3
## explicit
# github.com/onsi/gomega v1.7.1