mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-02 12:06:48 -04:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa331e52b8 | ||
|
|
c752f3529b | ||
|
|
3e26be9845 | ||
|
|
37b118f4fb | ||
|
|
9e64f7dd66 | ||
|
|
ac0c3db472 | ||
|
|
f185e1a5e3 | ||
|
|
ac7de7e8de | ||
|
|
706891e92b | ||
|
|
fa238809ae | ||
|
|
737a99b1c8 | ||
|
|
122d6e5f0d | ||
|
|
36de219eb7 | ||
|
|
bb6c883761 | ||
|
|
1e5d104524 | ||
|
|
946f5f130c | ||
|
|
a8c73f1ff2 | ||
|
|
d60fe437ef | ||
|
|
8280fdedb7 | ||
|
|
fdf9726c37 | ||
|
|
816d0c0820 | ||
|
|
5b4d009f55 | ||
|
|
fce4816a0d | ||
|
|
676b5c2287 | ||
|
|
d5bd30474c | ||
|
|
75a8c0c73e | ||
|
|
c4bfdaeaf3 | ||
|
|
5dbd91038a | ||
|
|
21b8b2827e | ||
|
|
3cff48437e | ||
|
|
ffb8586795 |
@@ -11,6 +11,8 @@
|
||||
* [ ] You've read through your own file changes for silly mistakes etc
|
||||
|
||||
<!--
|
||||
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
|
||||
Be sure to name your PR with an imperative e.g. 'Add worktrees view', and make sure the title
|
||||
is suitable to be included as a bullet point in release notes (i.e. phrased from a user's point
|
||||
of view).
|
||||
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for examples
|
||||
-->
|
||||
|
||||
@@ -190,6 +190,9 @@ gui:
|
||||
# This can be toggled from within Lazygit with the '`' key, but that will not change the default.
|
||||
showFileTree: true
|
||||
|
||||
# If true, add a "/" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level.
|
||||
showRootItemInFileTree: true
|
||||
|
||||
# If true, show the number of lines changed per file in the Files view
|
||||
showNumstatInFilesView: false
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ require (
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250605111917-fc5387961412
|
||||
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||
|
||||
@@ -194,8 +194,8 @@ github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2Paiis
|
||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e h1:NuSXZHAUSo+dSAYArlaDpmKgxVpojuKvNcRkYJcQxkY=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250605111917-fc5387961412 h1:8z1CpdCy9nzdj47lSLbDbCVmR5MgXsknYsuuHpzYk5M=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20250605111917-fc5387961412/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
|
||||
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a h1:UDeJ3EBk04bXDLOPvuqM3on8HvyJfISw0+UMqW+0a4g=
|
||||
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a/go.mod h1:FSWDLKT0NQpntbDd1H3lbz51fhCVlMzy/J0S6nM727Q=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
||||
|
||||
@@ -96,19 +96,23 @@ func (self *RemoteLoader) getRemoteBranchesByRemoteName() (map[string][]*models.
|
||||
|
||||
cmdArgs := NewGitCmd("for-each-ref").
|
||||
Arg(fmt.Sprintf("--sort=%s", sortOrder)).
|
||||
Arg("--format=%(refname:short)").
|
||||
Arg("--format=%(refname)").
|
||||
Arg("refs/remotes").
|
||||
ToArgv()
|
||||
|
||||
err := self.cmd.New(cmdArgs).DontLog().RunAndProcessLines(func(line string) (bool, error) {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
split := strings.SplitN(line, "/", 2)
|
||||
if len(split) != 2 {
|
||||
split := strings.SplitN(line, "/", 4)
|
||||
if len(split) != 4 {
|
||||
return false, nil
|
||||
}
|
||||
remoteName := split[2]
|
||||
name := split[3]
|
||||
|
||||
if name == "HEAD" {
|
||||
return false, nil
|
||||
}
|
||||
remoteName := split[0]
|
||||
name := split[1]
|
||||
|
||||
_, ok := remoteBranchesByRemoteName[remoteName]
|
||||
if !ok {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -295,14 +294,10 @@ const (
|
||||
Token
|
||||
)
|
||||
|
||||
// Whenever we're asked for a password we just enter a newline, which will
|
||||
// eventually cause the command to fail.
|
||||
// Whenever we're asked for a password we return a nil channel to tell the
|
||||
// caller to kill the process.
|
||||
var failPromptFn = func(CredentialType) <-chan string {
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
ch <- "\n"
|
||||
}()
|
||||
return ch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj *CmdObj) error {
|
||||
@@ -340,7 +335,7 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest(
|
||||
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
|
||||
|
||||
go utils.Safe(func() {
|
||||
self.processOutput(tr, handler.stdinPipe, promptUserForCredential, cmdObj.GetTask())
|
||||
self.processOutput(tr, handler.stdinPipe, promptUserForCredential, cmdObj)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -349,9 +344,10 @@ func (self *cmdObjRunner) processOutput(
|
||||
reader io.Reader,
|
||||
writer io.Writer,
|
||||
promptUserForCredential func(CredentialType) <-chan string,
|
||||
task gocui.Task,
|
||||
cmdObj *CmdObj,
|
||||
) {
|
||||
checkForCredentialRequest := self.getCheckForCredentialRequestFunc()
|
||||
task := cmdObj.GetTask()
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
scanner.Split(bufio.ScanBytes)
|
||||
@@ -360,16 +356,26 @@ func (self *cmdObjRunner) processOutput(
|
||||
askFor, ok := checkForCredentialRequest(newBytes)
|
||||
if ok {
|
||||
responseChan := promptUserForCredential(askFor)
|
||||
if task != nil {
|
||||
task.Pause()
|
||||
}
|
||||
toInput := <-responseChan
|
||||
if task != nil {
|
||||
task.Continue()
|
||||
}
|
||||
// If the return data is empty we don't write anything to stdin
|
||||
if toInput != "" {
|
||||
_, _ = writer.Write([]byte(toInput))
|
||||
if responseChan == nil {
|
||||
// Returning a nil channel means we should kill the process.
|
||||
// Note that we don't break the loop after this, because we
|
||||
// still need to drain the output, otherwise the Wait() call
|
||||
// later might block.
|
||||
if err := Kill(cmdObj.GetCmd()); err != nil {
|
||||
self.log.Error(err)
|
||||
}
|
||||
} else {
|
||||
if task != nil {
|
||||
task.Pause()
|
||||
}
|
||||
toInput := <-responseChan
|
||||
if task != nil {
|
||||
task.Continue()
|
||||
}
|
||||
// If the return data is empty we don't write anything to stdin
|
||||
if toInput != "" {
|
||||
_, _ = writer.Write([]byte(toInput))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,8 +120,8 @@ func TestProcessOutput(t *testing.T) {
|
||||
reader := strings.NewReader(scenario.output)
|
||||
writer := &strings.Builder{}
|
||||
|
||||
task := gocui.NewFakeTask()
|
||||
runner.processOutput(reader, writer, toChanFn(scenario.promptUserForCredential), task)
|
||||
cmdObj := &CmdObj{task: gocui.NewFakeTask()}
|
||||
runner.processOutput(reader, writer, toChanFn(scenario.promptUserForCredential), cmdObj)
|
||||
|
||||
if writer.String() != scenario.expectedToWrite {
|
||||
t.Errorf("expected to write '%s' but got '%s'", scenario.expectedToWrite, writer.String())
|
||||
|
||||
@@ -136,7 +136,7 @@ func NewAppConfig(
|
||||
}
|
||||
|
||||
func ConfigDir() string {
|
||||
_, filePath := findConfigFile("config.yml")
|
||||
_, filePath := findConfigFile(ConfigFilename)
|
||||
|
||||
return filepath.Dir(filePath)
|
||||
}
|
||||
|
||||
@@ -123,6 +123,8 @@ type GuiConfig struct {
|
||||
// If true, display the files in the file views as a tree. If false, display the files as a flat list.
|
||||
// This can be toggled from within Lazygit with the '`' key, but that will not change the default.
|
||||
ShowFileTree bool `yaml:"showFileTree"`
|
||||
// If true, add a "/" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level.
|
||||
ShowRootItemInFileTree bool `yaml:"showRootItemInFileTree"`
|
||||
// If true, show the number of lines changed per file in the Files view
|
||||
ShowNumstatInFilesView bool `yaml:"showNumstatInFilesView"`
|
||||
// If true, show a random tip in the command log when Lazygit starts
|
||||
@@ -764,6 +766,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
ShowBottomLine: true,
|
||||
ShowPanelJumps: true,
|
||||
ShowFileTree: true,
|
||||
ShowRootItemInFileTree: true,
|
||||
ShowNumstatInFilesView: false,
|
||||
ShowRandomTip: true,
|
||||
ShowIcons: false,
|
||||
|
||||
@@ -29,7 +29,7 @@ var (
|
||||
func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
|
||||
viewModel := filetree.NewCommitFileTreeViewModel(
|
||||
func() []*models.CommitFile { return c.Model().CommitFiles },
|
||||
c.Log,
|
||||
c.Common,
|
||||
c.UserConfig().Gui.ShowFileTree,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,14 +24,14 @@ var (
|
||||
func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
|
||||
viewModel := filetree.NewFileTreeViewModel(
|
||||
func() []*models.File { return c.Model().Files },
|
||||
c.Log,
|
||||
c.Common,
|
||||
c.UserConfig().Gui.ShowFileTree,
|
||||
)
|
||||
|
||||
getDisplayStrings := func(_ int, _ int) [][]string {
|
||||
showFileIcons := icons.IsIconEnabled() && c.UserConfig().Gui.ShowFileIcons
|
||||
showNumstat := c.UserConfig().Gui.ShowNumstatInFilesView
|
||||
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat, &c.UserConfig().Gui.CustomIcons)
|
||||
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat, &c.UserConfig().Gui.CustomIcons, c.UserConfig().Gui.ShowRootItemInFileTree)
|
||||
return lo.Map(lines, func(line string, _ int) []string {
|
||||
return []string{line}
|
||||
})
|
||||
|
||||
@@ -361,7 +361,7 @@ func (self *BasicCommitsController) newBranch(commit *models.Commit) error {
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) createResetMenu(commit *models.Commit) error {
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(commit.Hash())
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(commit.Hash(), commit.Hash())
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) checkout(commit *models.Commit) error {
|
||||
|
||||
@@ -326,7 +326,8 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc
|
||||
LabelColumns: []string{upstreamResetOptions},
|
||||
OpensMenu: true,
|
||||
OnPress: func() error {
|
||||
err := self.c.Helpers().Refs.CreateGitResetMenu(upstream)
|
||||
// We only can invoke this when the remote branch is stored locally, so using the selectedBranch here is fine.
|
||||
err := self.c.Helpers().Refs.CreateGitResetMenu(upstream, selectedBranch.FullUpstreamRefName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -686,7 +687,7 @@ func (self *BranchesController) createSortMenu() error {
|
||||
}
|
||||
|
||||
func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error {
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.Name)
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.Name, selectedBranch.FullRefName())
|
||||
}
|
||||
|
||||
func (self *BranchesController) rename(branch *models.Branch) error {
|
||||
|
||||
@@ -1144,7 +1144,7 @@ func (self *FilesController) stash() error {
|
||||
}
|
||||
|
||||
func (self *FilesController) createResetToUpstreamMenu() error {
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu("@{upstream}")
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu("@{upstream}", "@{upstream}")
|
||||
}
|
||||
|
||||
func (self *FilesController) handleToggleDirCollapsed() error {
|
||||
|
||||
@@ -448,8 +448,6 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
|
||||
self.c.Mutexes().RefreshingBranchesMutex.Lock()
|
||||
defer self.c.Mutexes().RefreshingBranchesMutex.Unlock()
|
||||
|
||||
prevSelectedBranch := self.c.Contexts().Branches.GetSelected()
|
||||
|
||||
reflogCommits := self.c.Model().FilteredReflogCommits
|
||||
if self.c.Modes().Filtering.Active() && self.c.AppState.LocalBranchSortOrder == "recency" {
|
||||
// in filter mode we filter our reflog commits to just those containing the path
|
||||
@@ -484,6 +482,8 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
|
||||
prevSelectedBranch := self.c.Contexts().Branches.GetSelected()
|
||||
|
||||
self.c.Model().Branches = branches
|
||||
|
||||
if refreshWorktrees {
|
||||
|
||||
@@ -231,7 +231,7 @@ func (self *RefsHelper) CreateSortOrderMenu(sortOptionsOrder []string, onSelecte
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RefsHelper) CreateGitResetMenu(ref string) error {
|
||||
func (self *RefsHelper) CreateGitResetMenu(name string, ref string) error {
|
||||
type strengthWithKey struct {
|
||||
strength string
|
||||
label string
|
||||
@@ -249,7 +249,7 @@ func (self *RefsHelper) CreateGitResetMenu(ref string) error {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: []string{
|
||||
row.label,
|
||||
style.FgRed.Sprintf("reset --%s %s", row.strength, ref),
|
||||
style.FgRed.Sprintf("reset --%s %s", row.strength, name),
|
||||
},
|
||||
OnPress: func() error {
|
||||
self.c.LogAction("Reset")
|
||||
@@ -261,7 +261,7 @@ func (self *RefsHelper) CreateGitResetMenu(ref string) error {
|
||||
})
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: fmt.Sprintf("%s %s", self.c.Tr.ResetTo, ref),
|
||||
Title: fmt.Sprintf("%s %s", self.c.Tr.ResetTo, name),
|
||||
Items: menuItems,
|
||||
})
|
||||
}
|
||||
@@ -325,13 +325,10 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest
|
||||
if suggestedBranchName == "" {
|
||||
var err error
|
||||
|
||||
suggestedBranchName, err = utils.ResolveTemplate(self.c.UserConfig().Git.BranchPrefix, nil, template.FuncMap{
|
||||
"runCommand": self.c.Git().Custom.TemplateFunctionRunCommand,
|
||||
})
|
||||
suggestedBranchName, err = self.getSuggestedBranchName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
suggestedBranchName = strings.ReplaceAll(suggestedBranchName, "\t", " ")
|
||||
}
|
||||
|
||||
refresh := func() error {
|
||||
@@ -399,16 +396,21 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
|
||||
return err
|
||||
}
|
||||
|
||||
withNewBranchNamePrompt := func(baseBranchName string, f func(string, string) error) {
|
||||
withNewBranchNamePrompt := func(baseBranchName string, f func(string, string) error) error {
|
||||
prompt := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.NewBranchNameBranchOff,
|
||||
map[string]string{
|
||||
"branchName": baseBranchName,
|
||||
},
|
||||
)
|
||||
suggestedBranchName, err := self.getSuggestedBranchName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.c.Prompt(types.PromptOpts{
|
||||
Title: prompt,
|
||||
Title: prompt,
|
||||
InitialContent: suggestedBranchName,
|
||||
HandleConfirm: func(response string) error {
|
||||
self.c.LogAction(self.c.Tr.MoveCommitsToNewBranch)
|
||||
newBranchName := SanitizedBranchName(response)
|
||||
@@ -417,6 +419,7 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
|
||||
})
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
isMainBranch := lo.Contains(self.c.UserConfig().Git.MainBranches, currentBranch.Name)
|
||||
@@ -431,8 +434,7 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
|
||||
Title: self.c.Tr.MoveCommitsToNewBranch,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
|
||||
return nil
|
||||
return withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
|
||||
},
|
||||
})
|
||||
return nil
|
||||
@@ -452,17 +454,15 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
|
||||
{
|
||||
Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchFromBaseItem, shortBaseBranchName),
|
||||
OnPress: func() error {
|
||||
withNewBranchNamePrompt(shortBaseBranchName, func(currentBranch string, newBranchName string) error {
|
||||
return withNewBranchNamePrompt(shortBaseBranchName, func(currentBranch string, newBranchName string) error {
|
||||
return self.moveCommitsToNewBranchOffOfMainBranch(currentBranch, newBranchName, baseBranchRef)
|
||||
})
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchStackedItem, currentBranch.Name),
|
||||
OnPress: func() error {
|
||||
withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
|
||||
return nil
|
||||
return withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -587,3 +587,14 @@ func (self *RefsHelper) ParseRemoteBranchName(fullBranchName string) (string, st
|
||||
func IsSwitchBranchUncommittedChangesError(err error) bool {
|
||||
return strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch")
|
||||
}
|
||||
|
||||
func (self *RefsHelper) getSuggestedBranchName() (string, error) {
|
||||
suggestedBranchName, err := utils.ResolveTemplate(self.c.UserConfig().Git.BranchPrefix, nil, template.FuncMap{
|
||||
"runCommand": self.c.Git().Custom.TemplateFunctionRunCommand,
|
||||
})
|
||||
if err != nil {
|
||||
return suggestedBranchName, err
|
||||
}
|
||||
suggestedBranchName = strings.ReplaceAll(suggestedBranchName, "\t", " ")
|
||||
return suggestedBranchName, nil
|
||||
}
|
||||
|
||||
@@ -144,14 +144,6 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
|
||||
),
|
||||
Description: self.c.Tr.Pick,
|
||||
Tooltip: self.c.Tr.PickCommitTooltip,
|
||||
// Not displaying this because we only want to display it when a TODO commit
|
||||
// is selected. A keybinding is displayed in the options view if Display is true,
|
||||
// and if it's not disabled, but if we disable it whenever a non-TODO commit is
|
||||
// selected, we'll be preventing pulls from happening within the commits view
|
||||
// (given they both use the 'p' key). Some approaches that come to mind:
|
||||
// * Allow a disabled keybinding to conditionally fallback to a global keybinding
|
||||
// * Allow a separate way of deciding whether a keybinding is displayed in the options view
|
||||
DisplayOnScreen: false,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Commits.CreateFixupCommit),
|
||||
@@ -623,9 +615,7 @@ func (self *LocalCommitsController) pick(selectedCommits []*models.Commit) error
|
||||
return self.updateTodos(todo.Pick, selectedCommits)
|
||||
}
|
||||
|
||||
// at this point we aren't actually rebasing so we will interpret this as an
|
||||
// attempt to pull. We might revoke this later after enabling configurable keybindings
|
||||
return self.pullFiles()
|
||||
panic("should be disabled when not rebasing")
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand, startIdx int, endIdx int) error {
|
||||
@@ -1476,8 +1466,7 @@ func (self *LocalCommitsController) pickEnabled(selectedCommits []*models.Commit
|
||||
}
|
||||
|
||||
if !self.isRebasing() {
|
||||
// if not rebasing, we're going to do a pull so we don't care about the selection
|
||||
return nil
|
||||
return &types.DisabledReason{Text: self.c.Tr.PickIsOnlyAllowedDuringRebase, AllowFurtherDispatching: true}
|
||||
}
|
||||
|
||||
return self.midRebaseCommandEnabled(selectedCommits, startIdx, endIdx)
|
||||
|
||||
@@ -158,7 +158,7 @@ func (self *RemoteBranchesController) createSortMenu() error {
|
||||
}
|
||||
|
||||
func (self *RemoteBranchesController) createResetMenu(selectedBranch *models.RemoteBranch) error {
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.FullName())
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.FullName(), selectedBranch.FullRefName())
|
||||
}
|
||||
|
||||
func (self *RemoteBranchesController) setAsUpstream(selectedBranch *models.RemoteBranch) error {
|
||||
|
||||
@@ -302,7 +302,7 @@ func (self *TagsController) push(tag *models.Tag) error {
|
||||
}
|
||||
|
||||
func (self *TagsController) createResetMenu(tag *models.Tag) error {
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(tag.Name)
|
||||
return self.c.Helpers().Refs.CreateGitResetMenu(tag.Name, tag.FullRefName())
|
||||
}
|
||||
|
||||
func (self *TagsController) create() error {
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func BuildTreeFromFiles(files []*models.File) *Node[models.File] {
|
||||
func BuildTreeFromFiles(files []*models.File, showRootItem bool) *Node[models.File] {
|
||||
root := &Node[models.File]{}
|
||||
|
||||
childrenMapsByNode := make(map[*Node[models.File]]map[string]*Node[models.File])
|
||||
|
||||
var curr *Node[models.File]
|
||||
for _, file := range files {
|
||||
splitPath := split("./" + file.Path)
|
||||
splitPath := SplitFileTreePath(file.Path, showRootItem)
|
||||
curr = root
|
||||
outer:
|
||||
for i := range splitPath {
|
||||
@@ -63,19 +63,19 @@ func BuildTreeFromFiles(files []*models.File) *Node[models.File] {
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] {
|
||||
rootAux := BuildTreeFromCommitFiles(files)
|
||||
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile, showRootItem bool) *Node[models.CommitFile] {
|
||||
rootAux := BuildTreeFromCommitFiles(files, showRootItem)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
return &Node[models.CommitFile]{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func BuildTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFile] {
|
||||
func BuildTreeFromCommitFiles(files []*models.CommitFile, showRootItem bool) *Node[models.CommitFile] {
|
||||
root := &Node[models.CommitFile]{}
|
||||
|
||||
var curr *Node[models.CommitFile]
|
||||
for _, file := range files {
|
||||
splitPath := split("./" + file.Path)
|
||||
splitPath := SplitFileTreePath(file.Path, showRootItem)
|
||||
curr = root
|
||||
outer:
|
||||
for i := range splitPath {
|
||||
@@ -115,8 +115,8 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *Node[models.CommitFil
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromFiles(files []*models.File) *Node[models.File] {
|
||||
rootAux := BuildTreeFromFiles(files)
|
||||
func BuildFlatTreeFromFiles(files []*models.File, showRootItem bool) *Node[models.File] {
|
||||
rootAux := BuildTreeFromFiles(files, showRootItem)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
// from top down we have merge conflict files, then tracked file, then untracked
|
||||
@@ -160,3 +160,11 @@ func split(str string) []string {
|
||||
func join(strs []string) string {
|
||||
return strings.Join(strs, "/")
|
||||
}
|
||||
|
||||
func SplitFileTreePath(path string, showRootItem bool) []string {
|
||||
if showRootItem {
|
||||
return split("./" + path)
|
||||
}
|
||||
|
||||
return split(path)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
|
||||
func TestBuildTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *Node[models.File]
|
||||
name string
|
||||
files []*models.File
|
||||
showRootItem bool
|
||||
expected *Node[models.File]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
@@ -31,6 +32,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
@@ -51,6 +53,37 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory, not root item",
|
||||
files: []*models.File{
|
||||
{
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
path: "dir1",
|
||||
CompressionLevel: 0,
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Path: "dir1/a"},
|
||||
path: "dir1/a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Path: "dir1/b"},
|
||||
path: "dir1/b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.File{
|
||||
@@ -61,6 +94,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
@@ -92,6 +126,43 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed, no root item",
|
||||
files: []*models.File{
|
||||
{
|
||||
Path: "dir1/dir3/a",
|
||||
},
|
||||
{
|
||||
Path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
path: "dir1/dir3",
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Path: "dir1/dir3/a"},
|
||||
path: "dir1/dir3/a",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
path: "dir2/dir4",
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Path: "dir2/dir4/b"},
|
||||
path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.File{
|
||||
@@ -102,6 +173,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Path: "a",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
@@ -135,6 +207,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
Path: "a",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
@@ -164,7 +237,7 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromFiles(s.files)
|
||||
result := BuildTreeFromFiles(s.files, s.showRootItem)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
@@ -172,9 +245,10 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
|
||||
func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *Node[models.File]
|
||||
name string
|
||||
files []*models.File
|
||||
showRootItem bool
|
||||
expected *Node[models.File]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
@@ -194,6 +268,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
@@ -210,6 +285,33 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory, not root item",
|
||||
files: []*models.File{
|
||||
{
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Path: "dir1/a"},
|
||||
path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.File{Path: "dir1/b"},
|
||||
path: "dir1/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.File{
|
||||
@@ -220,6 +322,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Path: "dir2/b",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
@@ -236,6 +339,33 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed, no root item",
|
||||
files: []*models.File{
|
||||
{
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
Path: "dir2/b",
|
||||
},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
{
|
||||
File: &models.File{Path: "dir1/a"},
|
||||
path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.File{Path: "dir2/b"},
|
||||
path: "dir2/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.File{
|
||||
@@ -246,6 +376,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Path: "a",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
@@ -288,6 +419,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
Tracked: true,
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.File]{
|
||||
path: "",
|
||||
Children: []*Node[models.File]{
|
||||
@@ -322,7 +454,7 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromFiles(s.files)
|
||||
result := BuildFlatTreeFromFiles(s.files, s.showRootItem)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
@@ -330,9 +462,10 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
|
||||
func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *Node[models.CommitFile]
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
showRootItem bool
|
||||
expected *Node[models.CommitFile]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
@@ -352,6 +485,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.CommitFile]{
|
||||
path: "",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
@@ -372,6 +506,37 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory, not root item",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: &Node[models.CommitFile]{
|
||||
path: "",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
path: "dir1",
|
||||
CompressionLevel: 0,
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Path: "dir1/a"},
|
||||
path: "dir1/a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Path: "dir1/b"},
|
||||
path: "dir1/b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.CommitFile{
|
||||
@@ -382,6 +547,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
Path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.CommitFile]{
|
||||
path: "",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
@@ -413,6 +579,43 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed, no root item",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Path: "dir1/dir3/a",
|
||||
},
|
||||
{
|
||||
Path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: &Node[models.CommitFile]{
|
||||
path: "",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
path: "dir1/dir3",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Path: "dir1/dir3/a"},
|
||||
path: "dir1/dir3/a",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
path: "dir2/dir4",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Path: "dir2/dir4/b"},
|
||||
path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.CommitFile{
|
||||
@@ -423,6 +626,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
Path: "a",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.CommitFile]{
|
||||
path: "",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
@@ -446,7 +650,7 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromCommitFiles(s.files)
|
||||
result := BuildTreeFromCommitFiles(s.files, s.showRootItem)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
@@ -454,9 +658,10 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
|
||||
func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *Node[models.CommitFile]
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
showRootItem bool
|
||||
expected *Node[models.CommitFile]
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
@@ -476,6 +681,7 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.CommitFile]{
|
||||
path: "",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
@@ -492,6 +698,33 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory, not root item",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: &Node[models.CommitFile]{
|
||||
path: "",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
{
|
||||
File: &models.CommitFile{Path: "dir1/a"},
|
||||
path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Path: "dir1/b"},
|
||||
path: "dir1/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.CommitFile{
|
||||
@@ -502,6 +735,7 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
Path: "dir2/b",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.CommitFile]{
|
||||
path: "",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
@@ -528,6 +762,7 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
Path: "a",
|
||||
},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: &Node[models.CommitFile]{
|
||||
path: "",
|
||||
Children: []*Node[models.CommitFile]{
|
||||
@@ -546,7 +781,7 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromCommitFiles(s.files)
|
||||
result := BuildFlatTreeFromCommitFiles(s.files, s.showRootItem)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ICommitFileTree interface {
|
||||
@@ -21,7 +21,7 @@ type CommitFileTree struct {
|
||||
getFiles func() []*models.CommitFile
|
||||
tree *Node[models.CommitFile]
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
common *common.Common
|
||||
collapsedPaths *CollapsedPaths
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@ func (self *CommitFileTree) ExpandAll() {
|
||||
|
||||
var _ ICommitFileTree = &CommitFileTree{}
|
||||
|
||||
func NewCommitFileTree(getFiles func() []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileTree {
|
||||
func NewCommitFileTree(getFiles func() []*models.CommitFile, common *common.Common, showTree bool) *CommitFileTree {
|
||||
return &CommitFileTree{
|
||||
getFiles: getFiles,
|
||||
log: log,
|
||||
common: common,
|
||||
showTree: showTree,
|
||||
collapsedPaths: NewCollapsedPaths(),
|
||||
}
|
||||
@@ -94,10 +94,11 @@ func (self *CommitFileTree) GetAllFiles() []*models.CommitFile {
|
||||
}
|
||||
|
||||
func (self *CommitFileTree) SetTree() {
|
||||
showRootItem := self.common.UserConfig().Gui.ShowRootItemInFileTree
|
||||
if self.showTree {
|
||||
self.tree = BuildTreeFromCommitFiles(self.getFiles())
|
||||
self.tree = BuildTreeFromCommitFiles(self.getFiles(), showRootItem)
|
||||
} else {
|
||||
self.tree = BuildFlatTreeFromCommitFiles(self.getFiles())
|
||||
self.tree = BuildFlatTreeFromCommitFiles(self.getFiles(), showRootItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ICommitFileTreeViewModel interface {
|
||||
@@ -43,8 +43,8 @@ type CommitFileTreeViewModel struct {
|
||||
|
||||
var _ ICommitFileTreeViewModel = &CommitFileTreeViewModel{}
|
||||
|
||||
func NewCommitFileTreeViewModel(getFiles func() []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileTreeViewModel {
|
||||
fileTree := NewCommitFileTree(getFiles, log, showTree)
|
||||
func NewCommitFileTreeViewModel(getFiles func() []*models.CommitFile, common *common.Common, showTree bool) *CommitFileTreeViewModel {
|
||||
fileTree := NewCommitFileTree(getFiles, common, showTree)
|
||||
listCursor := traits.NewListCursor(fileTree.Len)
|
||||
return &CommitFileTreeViewModel{
|
||||
ICommitFileTree: fileTree,
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type FileTreeDisplayFilter int
|
||||
@@ -54,17 +54,17 @@ type FileTree struct {
|
||||
getFiles func() []*models.File
|
||||
tree *Node[models.File]
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
common *common.Common
|
||||
filter FileTreeDisplayFilter
|
||||
collapsedPaths *CollapsedPaths
|
||||
}
|
||||
|
||||
var _ IFileTree = &FileTree{}
|
||||
|
||||
func NewFileTree(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTree {
|
||||
func NewFileTree(getFiles func() []*models.File, common *common.Common, showTree bool) *FileTree {
|
||||
return &FileTree{
|
||||
getFiles: getFiles,
|
||||
log: log,
|
||||
common: common,
|
||||
showTree: showTree,
|
||||
filter: DisplayAll,
|
||||
collapsedPaths: NewCollapsedPaths(),
|
||||
@@ -168,10 +168,11 @@ func (self *FileTree) GetAllFiles() []*models.File {
|
||||
|
||||
func (self *FileTree) SetTree() {
|
||||
filesForDisplay := self.getFilesForDisplay()
|
||||
showRootItem := self.common.UserConfig().Gui.ShowRootItemInFileTree
|
||||
if self.showTree {
|
||||
self.tree = BuildTreeFromFiles(filesForDisplay)
|
||||
self.tree = BuildTreeFromFiles(filesForDisplay, showRootItem)
|
||||
} else {
|
||||
self.tree = BuildFlatTreeFromFiles(filesForDisplay)
|
||||
self.tree = BuildFlatTreeFromFiles(filesForDisplay, showRootItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type IFileTreeViewModel interface {
|
||||
@@ -28,8 +28,8 @@ type FileTreeViewModel struct {
|
||||
|
||||
var _ IFileTreeViewModel = &FileTreeViewModel{}
|
||||
|
||||
func NewFileTreeViewModel(getFiles func() []*models.File, log *logrus.Entry, showTree bool) *FileTreeViewModel {
|
||||
fileTree := NewFileTree(getFiles, log, showTree)
|
||||
func NewFileTreeViewModel(getFiles func() []*models.File, common *common.Common, showTree bool) *FileTreeViewModel {
|
||||
fileTree := NewFileTree(getFiles, common, showTree)
|
||||
listCursor := traits.NewListCursor(fileTree.Len)
|
||||
return &FileTreeViewModel{
|
||||
IFileTree: fileTree,
|
||||
|
||||
@@ -523,6 +523,13 @@ func (gui *Gui) checkForChangedConfigsThatDontAutoReload(oldConfig *config.UserC
|
||||
// resetState reuses the repo state from our repo state map, if the repo was
|
||||
// open before; otherwise it creates a new one.
|
||||
func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
|
||||
// Un-highlight the current view if there is one. The reason we do this is
|
||||
// that the repo we are switching to might have a different view focused,
|
||||
// and would then show an inactive highlight for the previous view.
|
||||
if oldCurrentView := gui.g.CurrentView(); oldCurrentView != nil {
|
||||
oldCurrentView.Highlight = false
|
||||
}
|
||||
|
||||
worktreePath := gui.git.RepoPaths.WorktreePath()
|
||||
|
||||
if state := gui.RepoStateMap[Repo(worktreePath)]; state != nil {
|
||||
|
||||
+13
-10
@@ -523,19 +523,22 @@ func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) callKeybindingHandler(binding *types.Binding) error {
|
||||
var disabledReason *types.DisabledReason
|
||||
if binding.GetDisabledReason != nil {
|
||||
disabledReason = binding.GetDisabledReason()
|
||||
}
|
||||
if disabledReason != nil {
|
||||
if disabledReason.ShowErrorInPanel {
|
||||
return errors.New(disabledReason.Text)
|
||||
}
|
||||
if disabledReason := binding.GetDisabledReason(); disabledReason != nil {
|
||||
if disabledReason.AllowFurtherDispatching {
|
||||
return &types.ErrKeybindingNotHandled{DisabledReason: disabledReason}
|
||||
}
|
||||
|
||||
if len(disabledReason.Text) > 0 {
|
||||
gui.c.ErrorToast(gui.Tr.DisabledMenuItemPrefix + disabledReason.Text)
|
||||
if disabledReason.ShowErrorInPanel {
|
||||
return errors.New(disabledReason.Text)
|
||||
}
|
||||
|
||||
if len(disabledReason.Text) > 0 {
|
||||
gui.c.ErrorToast(gui.Tr.DisabledMenuItemPrefix + disabledReason.Text)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return binding.Handler()
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
@@ -121,7 +123,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
}
|
||||
|
||||
_, err := setViewFromDimensions(context)
|
||||
if err != nil && !gocui.IsUnknownView(err) {
|
||||
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -134,7 +136,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
|
||||
for _, context := range gui.transientContexts() {
|
||||
view, err := gui.g.View(context.GetViewName())
|
||||
if err != nil && !gocui.IsUnknownView(err) {
|
||||
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
|
||||
return err
|
||||
}
|
||||
view.Visible = gui.helpers.Window.GetViewNameForWindow(context.GetWindowName()) == context.GetViewName()
|
||||
|
||||
@@ -2,6 +2,7 @@ package popup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
@@ -80,6 +81,16 @@ func (self *PopupHandler) WithWaitingStatusSync(message string, f func() error)
|
||||
}
|
||||
|
||||
func (self *PopupHandler) ErrorHandler(err error) error {
|
||||
var notHandledError *types.ErrKeybindingNotHandled
|
||||
if errors.As(err, ¬HandledError) {
|
||||
if !notHandledError.DisabledReason.ShowErrorInPanel {
|
||||
if msg := notHandledError.DisabledReason.Text; len(msg) > 0 {
|
||||
self.ErrorToast(self.Tr.DisabledMenuItemPrefix + msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Need to set bold here explicitly; otherwise it gets cancelled by the red colouring.
|
||||
coloredMessage := style.FgRed.SetBold().Sprint(strings.TrimSpace(err.Error()))
|
||||
if err := self.onErrorFn(); err != nil {
|
||||
|
||||
@@ -25,12 +25,13 @@ func RenderFileTree(
|
||||
showFileIcons bool,
|
||||
showNumstat bool,
|
||||
customIconsConfig *config.CustomIconsConfig,
|
||||
showRootItem bool,
|
||||
) []string {
|
||||
collapsedPaths := tree.CollapsedPaths()
|
||||
return renderAux(tree.GetRoot().Raw(), collapsedPaths, -1, -1, func(node *filetree.Node[models.File], treeDepth int, visualDepth int, isCollapsed bool) string {
|
||||
fileNode := filetree.NewFileNode(node)
|
||||
|
||||
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node, customIconsConfig)
|
||||
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node, customIconsConfig, showRootItem)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -120,8 +121,9 @@ func getFileLine(
|
||||
submoduleConfigs []*models.SubmoduleConfig,
|
||||
node *filetree.Node[models.File],
|
||||
customIconsConfig *config.CustomIconsConfig,
|
||||
showRootItem bool,
|
||||
) string {
|
||||
name := fileNameAtDepth(node, treeDepth)
|
||||
name := fileNameAtDepth(node, treeDepth, showRootItem)
|
||||
output := ""
|
||||
|
||||
var nameColor style.TextStyle
|
||||
@@ -297,7 +299,7 @@ func getColorForChangeStatus(changeStatus string) style.TextStyle {
|
||||
}
|
||||
}
|
||||
|
||||
func fileNameAtDepth(node *filetree.Node[models.File], depth int) string {
|
||||
func fileNameAtDepth(node *filetree.Node[models.File], depth int, showRootItem bool) string {
|
||||
splitName := split(node.GetInternalPath())
|
||||
if depth == 0 && splitName[0] == "." {
|
||||
if len(splitName) == 1 {
|
||||
@@ -308,7 +310,7 @@ func fileNameAtDepth(node *filetree.Node[models.File], depth int) string {
|
||||
name := join(splitName[depth:])
|
||||
|
||||
if node.File != nil && node.File.IsRename() {
|
||||
splitPrevName := split("./" + node.File.PreviousPath)
|
||||
splitPrevName := filetree.SplitFileTreePath(node.File.PreviousPath, showRootItem)
|
||||
|
||||
prevName := node.File.PreviousPath
|
||||
// if the file has just been renamed inside the same directory, we can shave off
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/gookit/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -25,6 +26,7 @@ func TestRenderFileTree(t *testing.T) {
|
||||
files []*models.File
|
||||
collapsedPaths []string
|
||||
showLineChanges bool
|
||||
showRootItem bool
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
@@ -37,7 +39,8 @@ func TestRenderFileTree(t *testing.T) {
|
||||
files: []*models.File{
|
||||
{Path: "test", ShortStatus: " M", HasStagedChanges: true},
|
||||
},
|
||||
expected: []string{" M test"},
|
||||
showRootItem: true,
|
||||
expected: []string{" M test"},
|
||||
},
|
||||
{
|
||||
name: "numstat",
|
||||
@@ -48,6 +51,7 @@ func TestRenderFileTree(t *testing.T) {
|
||||
{Path: "test4", ShortStatus: " M", HasStagedChanges: true, LinesAdded: 0, LinesDeleted: 0},
|
||||
},
|
||||
showLineChanges: true,
|
||||
showRootItem: true,
|
||||
expected: []string{
|
||||
"▼ /",
|
||||
" M test +1 -1",
|
||||
@@ -66,6 +70,7 @@ func TestRenderFileTree(t *testing.T) {
|
||||
{Path: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Path: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: toStringSlice(
|
||||
`
|
||||
▼ /
|
||||
@@ -80,6 +85,30 @@ func TestRenderFileTree(t *testing.T) {
|
||||
),
|
||||
collapsedPaths: []string{"./dir1"},
|
||||
},
|
||||
{
|
||||
name: "big example without root item",
|
||||
files: []*models.File{
|
||||
{Path: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Path: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Path: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
{Path: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Path: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Path: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: toStringSlice(
|
||||
`
|
||||
▶ dir1
|
||||
▼ dir2
|
||||
▼ dir2
|
||||
M file3
|
||||
M file4
|
||||
M file5
|
||||
M file1
|
||||
`,
|
||||
),
|
||||
collapsedPaths: []string{"dir1"},
|
||||
},
|
||||
}
|
||||
|
||||
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
||||
@@ -87,12 +116,14 @@ func TestRenderFileTree(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
viewModel := filetree.NewFileTree(func() []*models.File { return s.files }, utils.NewDummyLog(), true)
|
||||
common := common.NewDummyCommon()
|
||||
common.UserConfig().Gui.ShowRootItemInFileTree = s.showRootItem
|
||||
viewModel := filetree.NewFileTree(func() []*models.File { return s.files }, common, true)
|
||||
viewModel.SetTree()
|
||||
for _, path := range s.collapsedPaths {
|
||||
viewModel.ToggleCollapsed(path)
|
||||
}
|
||||
result := RenderFileTree(viewModel, nil, false, s.showLineChanges, &config.CustomIconsConfig{})
|
||||
result := RenderFileTree(viewModel, nil, false, s.showLineChanges, &config.CustomIconsConfig{}, s.showRootItem)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
@@ -104,6 +135,7 @@ func TestRenderCommitFileTree(t *testing.T) {
|
||||
root *filetree.FileNode
|
||||
files []*models.CommitFile
|
||||
collapsedPaths []string
|
||||
showRootItem bool
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
@@ -116,7 +148,8 @@ func TestRenderCommitFileTree(t *testing.T) {
|
||||
files: []*models.CommitFile{
|
||||
{Path: "test", ChangeStatus: "A"},
|
||||
},
|
||||
expected: []string{"A test"},
|
||||
showRootItem: true,
|
||||
expected: []string{"A test"},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
@@ -128,6 +161,7 @@ func TestRenderCommitFileTree(t *testing.T) {
|
||||
{Path: "dir2/file5", ChangeStatus: "M"},
|
||||
{Path: "file1", ChangeStatus: "M"},
|
||||
},
|
||||
showRootItem: true,
|
||||
expected: toStringSlice(
|
||||
`
|
||||
▼ /
|
||||
@@ -142,6 +176,30 @@ func TestRenderCommitFileTree(t *testing.T) {
|
||||
),
|
||||
collapsedPaths: []string{"./dir1"},
|
||||
},
|
||||
{
|
||||
name: "big example without root item",
|
||||
files: []*models.CommitFile{
|
||||
{Path: "dir1/file2", ChangeStatus: "M"},
|
||||
{Path: "dir1/file3", ChangeStatus: "A"},
|
||||
{Path: "dir2/dir2/file3", ChangeStatus: "D"},
|
||||
{Path: "dir2/dir2/file4", ChangeStatus: "M"},
|
||||
{Path: "dir2/file5", ChangeStatus: "M"},
|
||||
{Path: "file1", ChangeStatus: "M"},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: toStringSlice(
|
||||
`
|
||||
▶ dir1
|
||||
▼ dir2
|
||||
▼ dir2
|
||||
D file3
|
||||
M file4
|
||||
M file5
|
||||
M file1
|
||||
`,
|
||||
),
|
||||
collapsedPaths: []string{"dir1"},
|
||||
},
|
||||
}
|
||||
|
||||
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
||||
@@ -151,7 +209,9 @@ func TestRenderCommitFileTree(t *testing.T) {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
|
||||
viewModel := filetree.NewCommitFileTreeViewModel(func() []*models.CommitFile { return s.files }, utils.NewDummyLog(), true)
|
||||
common := common.NewDummyCommon()
|
||||
common.UserConfig().Gui.ShowRootItemInFileTree = s.showRootItem
|
||||
viewModel := filetree.NewCommitFileTreeViewModel(func() []*models.CommitFile { return s.files }, common, true)
|
||||
viewModel.SetRef(models.NewCommit(hashPool, models.NewCommitOpts{Hash: "1234"}))
|
||||
viewModel.SetTree()
|
||||
for _, path := range s.collapsedPaths {
|
||||
|
||||
@@ -202,6 +202,10 @@ type DisabledReason struct {
|
||||
// error panel instead. This is useful if the text is very long, or if it is
|
||||
// important enough to show it more prominently, or both.
|
||||
ShowErrorInPanel bool
|
||||
|
||||
// If true, the keybinding dispatch mechanism will continue to look for
|
||||
// other handlers for the keypress.
|
||||
AllowFurtherDispatching bool
|
||||
}
|
||||
|
||||
type MenuWidget int
|
||||
|
||||
@@ -55,3 +55,15 @@ type KeybindingGuards struct {
|
||||
OutsideFilterMode Guard
|
||||
NoPopupPanel Guard
|
||||
}
|
||||
|
||||
type ErrKeybindingNotHandled struct {
|
||||
DisabledReason *DisabledReason
|
||||
}
|
||||
|
||||
func (e ErrKeybindingNotHandled) Error() string {
|
||||
return e.DisabledReason.Text
|
||||
}
|
||||
|
||||
func (e ErrKeybindingNotHandled) Unwrap() error {
|
||||
return gocui.ErrKeybindingNotHandled
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
@@ -78,7 +79,7 @@ func (gui *Gui) createAllViews() error {
|
||||
var err error
|
||||
for _, mapping := range gui.orderedViewNameMappings() {
|
||||
*mapping.viewPtr, err = gui.prepareView(mapping.name)
|
||||
if err != nil && !gocui.IsUnknownView(err) {
|
||||
if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -369,6 +369,7 @@ type TranslationSet struct {
|
||||
RewordNotSupported string
|
||||
ChangingThisActionIsNotAllowed string
|
||||
NotAllowedMidCherryPickOrRevert string
|
||||
PickIsOnlyAllowedDuringRebase string
|
||||
DroppingMergeRequiresSingleSelection string
|
||||
CherryPickCopy string
|
||||
CherryPickCopyTooltip string
|
||||
@@ -1459,6 +1460,7 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
RewordNotSupported: "Rewording commits while interactively rebasing is not currently supported",
|
||||
ChangingThisActionIsNotAllowed: "Changing this kind of rebase todo entry is not allowed",
|
||||
NotAllowedMidCherryPickOrRevert: "This action is not allowed while cherry-picking or reverting",
|
||||
PickIsOnlyAllowedDuringRebase: "This action is only allowed while rebasing",
|
||||
DroppingMergeRequiresSingleSelection: "Dropping a merge commit requires a single selected item",
|
||||
CherryPickCopy: "Copy (cherry-pick)",
|
||||
CherryPickCopyTooltip: "Mark commit as copied. Then, within the local commits view, you can press `{{.paste}}` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `{{.escape}}` to cancel the selection.",
|
||||
@@ -1529,7 +1531,7 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
DiscardFileChangesTitle: "Discard file changes",
|
||||
DiscardFileChangesPrompt: "Are you sure you want to remove changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.\nNote: This will also reset any active custom patches.",
|
||||
DisabledForGPG: "Feature not available for users using GPG.\n\nIf you are using a passphrase agent (e.g. gpg-agent) so that you don't have to type your passphrase when signing, you can enable this feature by adding\n\ngit:\n overrideGpg: true\n\nto your lazygit config file.",
|
||||
CreateRepo: "Not in a git repository. Create a new git repository? (y/n): ",
|
||||
CreateRepo: "Not in a git repository. Create a new git repository? (y/N): ",
|
||||
BareRepo: "You've attempted to open Lazygit in a bare repo but Lazygit does not yet support bare repos. Open most recent repo? (y/n) ",
|
||||
InitialBranch: "Branch name? (leave empty for git's default): ",
|
||||
NoRecentRepositories: "Must open lazygit in a git repository. No valid recent repositories. Exiting.",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -314,7 +315,7 @@ func (self *app) layout(g *gocui.Gui) error {
|
||||
g.FgColor = gocui.ColorGreen
|
||||
listView, err := g.SetView("list", 0, 0, maxX-1, maxY-descriptionViewHeight-keybindingsViewHeight-editorViewHeight-1, 0)
|
||||
if err != nil {
|
||||
if !gocui.IsUnknownView(err) {
|
||||
if !errors.Is(err, gocui.ErrUnknownView) {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -334,7 +335,7 @@ func (self *app) layout(g *gocui.Gui) error {
|
||||
|
||||
descriptionView, err := g.SetViewBeneath("description", "list", descriptionViewHeight)
|
||||
if err != nil {
|
||||
if !gocui.IsUnknownView(err) {
|
||||
if !errors.Is(err, gocui.ErrUnknownView) {
|
||||
return err
|
||||
}
|
||||
descriptionView.Title = "Test description"
|
||||
@@ -344,7 +345,7 @@ func (self *app) layout(g *gocui.Gui) error {
|
||||
|
||||
keybindingsView, err := g.SetViewBeneath("keybindings", "description", keybindingsViewHeight)
|
||||
if err != nil {
|
||||
if !gocui.IsUnknownView(err) {
|
||||
if !errors.Is(err, gocui.ErrUnknownView) {
|
||||
return err
|
||||
}
|
||||
keybindingsView.Title = "Keybindings"
|
||||
@@ -355,7 +356,7 @@ func (self *app) layout(g *gocui.Gui) error {
|
||||
|
||||
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
|
||||
if err != nil {
|
||||
if !gocui.IsUnknownView(err) {
|
||||
if !errors.Is(err, gocui.ErrUnknownView) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ var MoveCommitsToNewBranchKeepStacked = NewIntegrationTest(NewIntegrationTestArg
|
||||
Description: "Create a new branch from the commits that you accidentally made on the wrong branch; choosing stacked on current branch",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetUserConfig().Git.BranchPrefix = "myprefix/"
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("initial commit")
|
||||
shell.CloneIntoRemote("origin")
|
||||
@@ -42,12 +44,13 @@ var MoveCommitsToNewBranchKeepStacked = NewIntegrationTest(NewIntegrationTestArg
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("New branch name (branch is off of 'feature')")).
|
||||
InitialText(Equals("myprefix/")).
|
||||
Type("new branch").
|
||||
Confirm()
|
||||
|
||||
t.Views().Branches().
|
||||
Lines(
|
||||
Contains("new-branch").DoesNotContain("↑").IsSelected(),
|
||||
Contains("myprefix/new-branch").DoesNotContain("↑").IsSelected(),
|
||||
Contains("feature ✓"),
|
||||
Contains("master ✓"),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package branch
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ResetToDuplicateNamedTag = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Hard reset to a branch when a tag shares the same name",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.NewBranch("current-branch")
|
||||
|
||||
shell.EmptyCommit("other-branch-tag commit")
|
||||
shell.CreateLightweightTag("other-branch", "HEAD")
|
||||
|
||||
shell.EmptyCommit("other-branch commit")
|
||||
shell.NewBranch("other-branch")
|
||||
|
||||
shell.Checkout("current-branch")
|
||||
shell.EmptyCommit("current-branch commit")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().Lines(
|
||||
Contains("current-branch commit"),
|
||||
Contains("other-branch commit"),
|
||||
Contains("other-branch-tag commit"),
|
||||
)
|
||||
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("current-branch").IsSelected(),
|
||||
Contains("other-branch"),
|
||||
).
|
||||
SelectNextItem().
|
||||
Press(keys.Commits.ViewResetOptions)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Contains("Reset to other-branch")).
|
||||
Select(Contains("Hard reset")).
|
||||
Confirm()
|
||||
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("other-branch commit"),
|
||||
Contains("other-branch-tag commit"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
package branch
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ResetToDuplicateNamedUpstream = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Hard reset the current branch to an upstream branch when there is a competing tag name",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
CloneIntoRemote("origin").
|
||||
NewBranch("foo").
|
||||
EmptyCommit("commit 1").
|
||||
PushBranchAndSetUpstream("origin", "foo").
|
||||
EmptyCommit("commit 2").
|
||||
CreateLightweightTag("origin/foo", "HEAD")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().Lines(
|
||||
Contains("commit 2"),
|
||||
Contains("commit 1"),
|
||||
)
|
||||
t.Views().Tags().Focus().Lines(Contains("origin/foo"))
|
||||
|
||||
t.Views().Remotes().Focus().
|
||||
Lines(Contains("origin")).
|
||||
PressEnter()
|
||||
t.Views().RemoteBranches().IsFocused().
|
||||
Lines(Contains("foo")).
|
||||
Press(keys.Commits.ViewResetOptions)
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Contains("Reset to origin/foo")).
|
||||
Select(Contains("Hard reset")).
|
||||
Confirm()
|
||||
|
||||
t.Views().Commits().Lines(
|
||||
Contains("commit 1"),
|
||||
)
|
||||
|
||||
t.Views().Tags().Focus().
|
||||
Lines(Contains("origin/foo")).
|
||||
Press(keys.Commits.ViewResetOptions)
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Contains("Reset to origin/foo")).
|
||||
Select(Contains("Hard reset")).
|
||||
Confirm()
|
||||
|
||||
t.Views().Commits().Lines(
|
||||
Contains("commit 2"),
|
||||
Contains("commit 1"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var RenamedFilesNoRootItem = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Regression test for the display of renamed files in the file tree, when the root item is disabled",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetUserConfig().Gui.ShowRootItemInFileTree = false
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateDir("dir")
|
||||
shell.CreateDir("dir/nested")
|
||||
shell.CreateFileAndAdd("file1", "file1 content\n")
|
||||
shell.CreateFileAndAdd("dir/file2", "file2 content\n")
|
||||
shell.CreateFileAndAdd("dir/nested/file3", "file3 content\n")
|
||||
shell.Commit("initial commit")
|
||||
shell.RunCommand([]string{"git", "mv", "file1", "dir/file1"})
|
||||
shell.RunCommand([]string{"git", "mv", "dir/file2", "dir/file2-renamed"})
|
||||
shell.RunCommand([]string{"git", "mv", "dir/nested/file3", "file3"})
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Equals("▼ dir"),
|
||||
Equals(" R file1 → file1"),
|
||||
Equals(" R file2 → file2-renamed"),
|
||||
Equals("R dir/nested/file3 → file3"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var ResetToDuplicateNamedBranch = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Hard reset to a tag when a branch shares the same name",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.NewBranch("current-branch")
|
||||
|
||||
shell.EmptyCommit("other-branch-tag commit")
|
||||
shell.CreateLightweightTag("other-branch", "HEAD")
|
||||
|
||||
shell.EmptyCommit("other-branch commit")
|
||||
shell.NewBranch("other-branch")
|
||||
|
||||
shell.Checkout("current-branch")
|
||||
shell.EmptyCommit("current-branch commit")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().Lines(
|
||||
Contains("current-branch commit"),
|
||||
Contains("other-branch commit"),
|
||||
Contains("other-branch-tag commit"),
|
||||
)
|
||||
|
||||
t.Views().Tags().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("other-branch").IsSelected(),
|
||||
).
|
||||
Press(keys.Commits.ViewResetOptions)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Contains("Reset to other-branch")).
|
||||
Select(Contains("Hard reset")).
|
||||
Confirm()
|
||||
|
||||
t.Views().Commits().Lines(
|
||||
Contains("other-branch-tag commit"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -71,6 +71,8 @@ var tests = []*components.IntegrationTest{
|
||||
branch.RebaseToUpstream,
|
||||
branch.Rename,
|
||||
branch.Reset,
|
||||
branch.ResetToDuplicateNamedTag,
|
||||
branch.ResetToDuplicateNamedUpstream,
|
||||
branch.ResetToUpstream,
|
||||
branch.SelectCommitsOfCurrentBranch,
|
||||
branch.SetUpstream,
|
||||
@@ -207,6 +209,7 @@ var tests = []*components.IntegrationTest{
|
||||
file.RememberCommitMessageAfterFail,
|
||||
file.RenameSimilarityThresholdChange,
|
||||
file.RenamedFiles,
|
||||
file.RenamedFilesNoRootItem,
|
||||
file.StageChildrenRangeSelect,
|
||||
file.StageDeletedRangeSelect,
|
||||
file.StageRangeSelect,
|
||||
@@ -405,6 +408,7 @@ var tests = []*components.IntegrationTest{
|
||||
tag.ForceTagAnnotated,
|
||||
tag.ForceTagLightweight,
|
||||
tag.Reset,
|
||||
tag.ResetToDuplicateNamedBranch,
|
||||
ui.Accordion,
|
||||
ui.DisableSwitchTabWithPanelJumpKeys,
|
||||
ui.EmptyMenu,
|
||||
|
||||
@@ -569,6 +569,11 @@
|
||||
"description": "If true, display the files in the file views as a tree. If false, display the files as a flat list.\nThis can be toggled from within Lazygit with the '`' key, but that will not change the default.",
|
||||
"default": true
|
||||
},
|
||||
"showRootItemInFileTree": {
|
||||
"type": "boolean",
|
||||
"description": "If true, add a \"/\" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level.",
|
||||
"default": true
|
||||
},
|
||||
"showNumstatInFilesView": {
|
||||
"type": "boolean",
|
||||
"description": "If true, show the number of lines changed per file in the Files view",
|
||||
|
||||
+15
-42
@@ -786,7 +786,7 @@ func (g *Gui) MainLoop() error {
|
||||
}
|
||||
|
||||
func (g *Gui) handleError(err error) error {
|
||||
if err != nil && !IsQuit(err) && g.ErrorHandler != nil {
|
||||
if err != nil && !standardErrors.Is(err, ErrQuit) && g.ErrorHandler != nil {
|
||||
return g.ErrorHandler(err)
|
||||
}
|
||||
|
||||
@@ -1232,24 +1232,9 @@ func (g *Gui) draw(v *View) error {
|
||||
|
||||
if g.Cursor {
|
||||
if curview := g.currentView; curview != nil {
|
||||
vMaxX, vMaxY := curview.Size()
|
||||
if curview.cx < 0 {
|
||||
curview.cx = 0
|
||||
} else if curview.cx >= vMaxX {
|
||||
curview.cx = vMaxX - 1
|
||||
}
|
||||
if curview.cy < 0 {
|
||||
curview.cy = 0
|
||||
} else if curview.cy >= vMaxY {
|
||||
curview.cy = vMaxY - 1
|
||||
}
|
||||
|
||||
gMaxX, gMaxY := g.Size()
|
||||
cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
|
||||
// This test probably doesn't need to be here.
|
||||
// tcell is hiding cursor by setting coordinates outside of screen.
|
||||
// Keeping it here for now, as I'm not 100% sure :)
|
||||
if cx >= 0 && cx < gMaxX && cy >= 0 && cy < gMaxY {
|
||||
vMaxX, vMaxY := curview.InnerSize()
|
||||
if curview.cx >= 0 && curview.cx < vMaxX && curview.cy >= 0 && curview.cy < vMaxY {
|
||||
cx, cy := curview.x0+curview.cx+1, curview.y0+curview.cy+1
|
||||
Screen.ShowCursor(cx, cy)
|
||||
} else {
|
||||
Screen.HideCursor()
|
||||
@@ -1513,6 +1498,8 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
for _, kb := range g.keybindings {
|
||||
if kb.handler == nil {
|
||||
continue
|
||||
@@ -1521,13 +1508,13 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
|
||||
continue
|
||||
}
|
||||
if g.matchView(v, kb) {
|
||||
err := g.execKeybinding(v, kb)
|
||||
if IsKeybindingNotHandled(err) {
|
||||
matchingParentViewKb = nil
|
||||
break
|
||||
} else {
|
||||
err = g.execKeybinding(v, kb)
|
||||
if !errors.Is(err, ErrKeybindingNotHandled) {
|
||||
return err
|
||||
}
|
||||
|
||||
matchingParentViewKb = nil
|
||||
break
|
||||
}
|
||||
if v != nil && g.matchView(v.ParentView, kb) {
|
||||
matchingParentViewKb = kb
|
||||
@@ -1537,8 +1524,8 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
|
||||
}
|
||||
}
|
||||
if matchingParentViewKb != nil {
|
||||
err := g.execKeybinding(v.ParentView, matchingParentViewKb)
|
||||
if !IsKeybindingNotHandled(err) {
|
||||
err = g.execKeybinding(v.ParentView, matchingParentViewKb)
|
||||
if !errors.Is(err, ErrKeybindingNotHandled) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1551,9 +1538,9 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
|
||||
}
|
||||
|
||||
if globalKb != nil {
|
||||
return g.execKeybinding(v, globalKb)
|
||||
err = g.execKeybinding(v, globalKb)
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// execKeybinding executes a given keybinding
|
||||
@@ -1617,20 +1604,6 @@ func (g *Gui) isBlacklisted(k Key) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsUnknownView reports whether the contents of an error is "unknown view".
|
||||
func IsUnknownView(err error) bool {
|
||||
return err != nil && err.Error() == ErrUnknownView.Error()
|
||||
}
|
||||
|
||||
// IsQuit reports whether the contents of an error is "quit".
|
||||
func IsQuit(err error) bool {
|
||||
return err != nil && err.Error() == ErrQuit.Error()
|
||||
}
|
||||
|
||||
func IsKeybindingNotHandled(err error) bool {
|
||||
return err != nil && err.Error() == ErrKeybindingNotHandled.Error()
|
||||
}
|
||||
|
||||
func (g *Gui) Suspend() error {
|
||||
g.suspendedMutex.Lock()
|
||||
defer g.suspendedMutex.Unlock()
|
||||
|
||||
+7
-16
@@ -562,29 +562,19 @@ func max(a, b int) int {
|
||||
}
|
||||
|
||||
// SetCursor sets the cursor position of the view at the given point,
|
||||
// relative to the view. It checks if the position is valid.
|
||||
// relative to the view. It is allowed to set the position to a point outside
|
||||
// the visible portion of the view, or even outside the content of the view.
|
||||
// Clients are responsible for clamping to valid positions.
|
||||
func (v *View) SetCursor(x, y int) {
|
||||
maxX, maxY := v.InnerSize()
|
||||
if x < 0 || x >= maxX || y < 0 || y >= maxY {
|
||||
return
|
||||
}
|
||||
v.cx = x
|
||||
v.cy = y
|
||||
}
|
||||
|
||||
func (v *View) SetCursorX(x int) {
|
||||
maxX := v.InnerWidth()
|
||||
if x < 0 || x >= maxX {
|
||||
return
|
||||
}
|
||||
v.cx = x
|
||||
}
|
||||
|
||||
func (v *View) SetCursorY(y int) {
|
||||
maxY := v.InnerHeight()
|
||||
if y < 0 || y >= maxY {
|
||||
return
|
||||
}
|
||||
v.cy = y
|
||||
}
|
||||
|
||||
@@ -1685,10 +1675,11 @@ func (v *View) RenderTextArea() {
|
||||
func updatedCursorAndOrigin(prevOrigin int, size int, cursor int) (int, int) {
|
||||
var newViewCursor int
|
||||
newOrigin := prevOrigin
|
||||
usableSize := size - 1
|
||||
|
||||
if cursor > prevOrigin+size {
|
||||
newOrigin = cursor - size
|
||||
newViewCursor = size
|
||||
if cursor > prevOrigin+usableSize {
|
||||
newOrigin = cursor - usableSize
|
||||
newViewCursor = usableSize
|
||||
} else if cursor < prevOrigin {
|
||||
newOrigin = cursor
|
||||
newViewCursor = 0
|
||||
|
||||
Vendored
+1
-1
@@ -227,7 +227,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
||||
github.com/jesseduffield/go-git/v5/utils/sync
|
||||
github.com/jesseduffield/go-git/v5/utils/trace
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20250605111917-fc5387961412
|
||||
## explicit; go 1.12
|
||||
github.com/jesseduffield/gocui
|
||||
# github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a
|
||||
|
||||
Reference in New Issue
Block a user