mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 10:16:22 -04:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
954a4d2fbb | ||
|
|
6da1cf87b1 | ||
|
|
508b869773 | ||
|
|
59cc6843e6 | ||
|
|
f108fd2236 | ||
|
|
8081b59a02 | ||
|
|
26d180a50a | ||
|
|
10fe872c71 | ||
|
|
c74448f00d | ||
|
|
41ab7c44a0 | ||
|
|
3589a43682 | ||
|
|
ad70341139 | ||
|
|
ea93df571d | ||
|
|
bc21921001 | ||
|
|
07cbb62510 | ||
|
|
fc868cdda5 | ||
|
|
864a9ada57 | ||
|
|
f3e9d50d94 | ||
|
|
cdad0b998e | ||
|
|
64012d67a9 | ||
|
|
33d9d4dbf1 | ||
|
|
4c5e250ed8 | ||
|
|
1359fa14c1 | ||
|
|
c222a618a8 | ||
|
|
05c32e292e | ||
|
|
fb4f742416 | ||
|
|
276438b601 | ||
|
|
edec116ceb | ||
|
|
1aae1772ce | ||
|
|
a642395e9f | ||
|
|
70bfeddc90 | ||
|
|
e2a966443b | ||
|
|
0b13c3ca87 | ||
|
|
8b6766de79 | ||
|
|
c9371f812f | ||
|
|
e592d81b60 | ||
|
|
f2f50ccf75 | ||
|
|
75aed98c35 | ||
|
|
7f9818cfa2 | ||
|
|
679148449a | ||
|
|
bc00aeb10d | ||
|
|
4d258bd981 | ||
|
|
c465b0f2ff | ||
|
|
25160b671e | ||
|
|
20bfa42792 | ||
|
|
5a740e34c7 | ||
|
|
0aad599a9b | ||
|
|
b6c892a08a | ||
|
|
28d12e4e5d | ||
|
|
5d5e24a48e | ||
|
|
de598e55a6 | ||
|
|
0fd8392067 |
+1
-1
@@ -35,7 +35,7 @@ lazygit.exe
|
||||
|
||||
test/git_server/data
|
||||
|
||||
test/results/**
|
||||
test/_results/**
|
||||
|
||||
oryxBuildBinary
|
||||
__debug_bin
|
||||
|
||||
Vendored
+35
-4
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
type RemotesContext struct {
|
||||
*FilteredListViewModel[*models.Remote]
|
||||
*ListContextTrait
|
||||
*SearchHistory
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() },
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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: "Удаление",
|
||||
|
||||
@@ -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: "刪除中",
|
||||
|
||||
@@ -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:
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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`))
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
.vagrant/
|
||||
+21
@@ -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
@@ -0,0 +1,34 @@
|
||||
# Process List Library for Go [](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Vendored
+5
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user