diff options
Diffstat (limited to 'pkg/gui')
48 files changed, 1380 insertions, 315 deletions
diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index e4c3d7280..a6b0e77cb 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -105,7 +105,6 @@ func (self *MenuViewModel) GetNonModelItems() []*NonModelItem { menuItems := self.FilteredListViewModel.GetItems() var prevSection *types.MenuSection = nil for i, menuItem := range menuItems { - menuItem := menuItem if menuItem.Section != nil && menuItem.Section != prevSection { if prevSection != nil { result = append(result, &NonModelItem{ diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index b37be8667..f540dba87 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -75,7 +75,7 @@ func NewSubCommitsContext( endIdx, // Don't show the graph in the left/right view; we'd like to, but // it's too complicated: - shouldShowGraph(c) && viewModel.GetRefToShowDivergenceFrom() == "", + shouldShowGraph(c), git_commands.NewNullBisectInfo(), false, ) diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index 59908fe5e..c741cc769 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -14,10 +14,13 @@ type SuggestionsContext struct { } type SuggestionsContextState struct { - Suggestions []*types.Suggestion - OnConfirm func() error - OnClose func() error - AsyncHandler *tasks.AsyncHandler + Suggestions []*types.Suggestion + OnConfirm func() error + OnClose func() error + OnDeleteSuggestion func() error + AsyncHandler *tasks.AsyncHandler + + AllowEditSuggestion bool // FindSuggestions will take a string that the user has typed into a prompt // and return a slice of suggestions which match that string. diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index b08ddd0cd..62eda703e 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -100,14 +100,13 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty DisplayOnScreen: true, }, { - Key: opts.GetKey(opts.Config.Branches.RebaseBranch), - Handler: opts.Guards.OutsideFilterMode(self.rebase), - GetDisabledReason: self.require( - self.singleItemSelected(self.notRebasingOntoSelf), - ), - Description: self.c.Tr.RebaseBranch, - Tooltip: self.c.Tr.RebaseBranchTooltip, - DisplayOnScreen: true, + Key: opts.GetKey(opts.Config.Branches.RebaseBranch), + Handler: opts.Guards.OutsideFilterMode(self.withItem(self.rebase)), + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.RebaseBranch, + Tooltip: self.c.Tr.RebaseBranchTooltip, + OpensMenu: true, + DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch), @@ -205,6 +204,40 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc }, } + var disabledReason *types.DisabledReason + baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(selectedBranch, self.c.Model().MainBranches) + if err != nil { + return err + } + if baseBranch == "" { + baseBranch = self.c.Tr.CouldNotDetermineBaseBranch + disabledReason = &types.DisabledReason{Text: self.c.Tr.CouldNotDetermineBaseBranch} + } + shortBaseBranchName := helpers.ShortBranchName(baseBranch) + label := utils.ResolvePlaceholderString( + self.c.Tr.ViewDivergenceFromBaseBranch, + map[string]string{"baseBranch": shortBaseBranchName}, + ) + viewDivergenceFromBaseBranchItem := &types.MenuItem{ + LabelColumns: []string{label}, + Key: 'b', + 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(), shortBaseBranchName), + RefToShowDivergenceFrom: baseBranch, + Context: self.context(), + ShowBranchHeads: false, + }) + }, + DisabledReason: disabledReason, + } + unsetUpstreamItem := &types.MenuItem{ LabelColumns: []string{self.c.Tr.UnsetUpstream}, OnPress: func() error { @@ -312,6 +345,7 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc options := []*types.MenuItem{ viewDivergenceItem, + viewDivergenceFromBaseBranchItem, unsetUpstreamItem, setUpstreamItem, upstreamResetItem, @@ -598,19 +632,8 @@ func (self *BranchesController) merge() error { return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName) } -func (self *BranchesController) rebase() error { - selectedBranchName := self.context().GetSelected().Name - return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName) -} - -func (self *BranchesController) notRebasingOntoSelf(branch *models.Branch) *types.DisabledReason { - selectedBranchName := branch.Name - checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name - if selectedBranchName == checkedOutBranch { - return &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf} - } - - return nil +func (self *BranchesController) rebase(branch *models.Branch) error { + return self.c.Helpers().MergeAndRebase.RebaseOntoRef(branch.Name) } func (self *BranchesController) fastForward(branch *models.Branch) error { @@ -620,7 +643,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error { if !branch.RemoteBranchStoredLocally() { return errors.New(self.c.Tr.FwdNoLocalUpstream) } - if branch.HasCommitsToPush() { + if branch.IsAheadForPull() { return errors.New(self.c.Tr.FwdCommitsToPush) } diff --git a/pkg/gui/controllers/confirmation_controller.go b/pkg/gui/controllers/confirmation_controller.go index aa5617fa8..45bd16a45 100644 --- a/pkg/gui/controllers/confirmation_controller.go +++ b/pkg/gui/controllers/confirmation_controller.go @@ -1,6 +1,8 @@ package controllers import ( + "fmt" + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -39,6 +41,14 @@ func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) [ Key: opts.GetKey(opts.Config.Universal.TogglePanel), Handler: func() error { if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 { + subtitle := "" + if self.c.State().GetRepoState().GetCurrentPopupOpts().HandleDeleteSuggestion != nil { + // We assume that whenever things are deletable, they + // are also editable, so we show both keybindings + subtitle = fmt.Sprintf(self.c.Tr.SuggestionsSubtitle, + self.c.UserConfig.Keybinding.Universal.Remove, self.c.UserConfig.Keybinding.Universal.Edit) + } + self.c.Views().Suggestions.Subtitle = subtitle return self.c.ReplaceContext(self.c.Contexts().Suggestions) } return nil diff --git a/pkg/gui/controllers/custom_command_action.go b/pkg/gui/controllers/custom_command_action.go index f4de3218e..39777e70a 100644 --- a/pkg/gui/controllers/custom_command_action.go +++ b/pkg/gui/controllers/custom_command_action.go @@ -1,6 +1,7 @@ package controllers import ( + "slices" "strings" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" @@ -17,6 +18,7 @@ func (self *CustomCommandAction) Call() error { return self.c.Prompt(types.PromptOpts{ Title: self.c.Tr.CustomCommand, FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(), + AllowEditSuggestion: true, HandleConfirm: func(command string) error { if self.shouldSaveCommand(command) { self.c.GetAppState().CustomCommandsHistory = utils.Limit( @@ -32,13 +34,34 @@ func (self *CustomCommandAction) Call() error { self.c.OS().Cmd.NewShell(command), ) }, + HandleDeleteSuggestion: func(index int) error { + // index is the index in the _filtered_ list of suggestions, so we + // need to map it back to the full list. There's no really good way + // to do this, but fortunately we keep the items in the + // CustomCommandsHistory unique, which allows us to simply search + // for it by string. + item := self.c.Contexts().Suggestions.GetItems()[index].Value + fullIndex := lo.IndexOf(self.c.GetAppState().CustomCommandsHistory, item) + if fullIndex == -1 { + // Should never happen, but better be safe + return nil + } + + self.c.GetAppState().CustomCommandsHistory = slices.Delete( + self.c.GetAppState().CustomCommandsHistory, fullIndex, fullIndex+1) + self.c.SaveAppStateAndLogError() + self.c.Contexts().Suggestions.RefreshSuggestions() + return nil + }, }) } func (self *CustomCommandAction) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion { - history := self.c.GetAppState().CustomCommandsHistory + return func(input string) []*types.Suggestion { + history := self.c.GetAppState().CustomCommandsHistory - return helpers.FilterFunc(history, self.c.UserConfig.Gui.UseFuzzySearch()) + return helpers.FilterFunc(history, self.c.UserConfig.Gui.UseFuzzySearch())(input) + } } // this mimics the shell functionality `ignorespace` diff --git a/pkg/gui/controllers/custom_patch_options_menu_action.go b/pkg/gui/controllers/custom_patch_options_menu_action.go index 2d57f0ac0..f5099ae2e 100644 --- a/pkg/gui/controllers/custom_patch_options_menu_action.go +++ b/pkg/gui/controllers/custom_patch_options_menu_action.go @@ -217,7 +217,10 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error { _ = self.c.Helpers().Commits.PopCommitMessageContexts() self.c.LogAction(self.c.Tr.Actions.MovePatchIntoNewCommit) err := self.c.Git().Patch.PullPatchIntoNewCommit(self.c.Model().Commits, commitIndex, summary, description) - return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) + if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err); err != nil { + return err + } + return self.c.PushContext(self.c.Contexts().LocalCommits) }) }, }, diff --git a/pkg/gui/controllers/diffing_menu_action.go b/pkg/gui/controllers/diffing_menu_action.go index ec4006b1a..be44e471b 100644 --- a/pkg/gui/controllers/diffing_menu_action.go +++ b/pkg/gui/controllers/diffing_menu_action.go @@ -17,7 +17,6 @@ func (self *DiffingMenuAction) Call() error { menuItems := []*types.MenuItem{} for _, name := range names { - name := name menuItems = append(menuItems, []*types.MenuItem{ { Label: fmt.Sprintf("%s %s", self.c.Tr.Diff, name), diff --git a/pkg/gui/controllers/helpers/branches_helper.go b/pkg/gui/controllers/helpers/branches_helper.go index d9d6dbd9a..c07d1d72b 100644 --- a/pkg/gui/controllers/helpers/branches_helper.go +++ b/pkg/gui/controllers/helpers/branches_helper.go @@ -1,6 +1,8 @@ package helpers import ( + "strings" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" @@ -44,3 +46,7 @@ func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName st }, }) } + +func ShortBranchName(fullBranchName string) string { + return strings.TrimPrefix(strings.TrimPrefix(fullBranchName, "refs/heads/"), "refs/remotes/") +} diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 8c1265c15..1e60b5f08 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -159,6 +159,7 @@ func (self *ConfirmationHelper) prepareConfirmationPanel( suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc("")) suggestionsView.Visible = true suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel) + suggestionsView.Subtitle = "" } self.ResizeConfirmationPanel() @@ -223,6 +224,8 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ return err } + self.c.Contexts().Suggestions.State.AllowEditSuggestion = opts.AllowEditSuggestion + self.c.State().GetRepoState().SetCurrentPopupOpts(&opts) return self.c.PushContext(self.c.Contexts().Confirmation) @@ -270,10 +273,20 @@ func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts onClose := self.wrappedConfirmationFunction(cancel, opts.HandleClose) + onDeleteSuggestion := func() error { + if opts.HandleDeleteSuggestion == nil { + return nil + } + + idx := self.c.Contexts().Suggestions.GetSelectedLineIdx() + return opts.HandleDeleteSuggestion(idx) + } + self.c.Contexts().Confirmation.State.OnConfirm = onConfirm self.c.Contexts().Confirmation.State.OnClose = onClose self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm self.c.Contexts().Suggestions.State.OnClose = onClose + self.c.Contexts().Suggestions.State.OnDeleteSuggestion = onDeleteSuggestion return nil } @@ -284,6 +297,7 @@ func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() { self.c.Contexts().Confirmation.State.OnClose = noop self.c.Contexts().Suggestions.State.OnConfirm = noop self.c.Contexts().Suggestions.State.OnClose = noop + self.c.Contexts().Suggestions.State.OnDeleteSuggestion = noop } func (self *ConfirmationHelper) getSelectedSuggestionValue() string { @@ -354,7 +368,8 @@ func (self *ConfirmationHelper) resizeMenu() { if selectedItem != nil { tooltip = self.TooltipForMenuItem(selectedItem) } - tooltipHeight := getMessageHeight(true, tooltip, panelWidth) + 2 // plus 2 for the frame + contentWidth := panelWidth - 2 // minus 2 for the frame + tooltipHeight := getMessageHeight(true, tooltip, contentWidth) + 2 // plus 2 for the frame _, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0) } diff --git a/pkg/gui/controllers/helpers/fixup_helper.go b/pkg/gui/controllers/helpers/fixup_helper.go index b60d48f4f..7177398bb 100644 --- a/pkg/gui/controllers/helpers/fixup_helper.go +++ b/pkg/gui/controllers/helpers/fixup_helper.go @@ -5,13 +5,13 @@ import ( "fmt" "regexp" "strings" - "sync" "github.com/jesseduffield/generics/set" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" + "golang.org/x/sync/errgroup" ) type FixupHelper struct { @@ -26,7 +26,17 @@ func NewFixupHelper( } } -type deletedLineInfo struct { +// hunk describes the lines in a diff hunk. Used for two distinct cases: +// +// - when the hunk contains some deleted lines. Because we're diffing with a +// context of 0, all deleted lines always come first, and then the added lines +// (if any). In this case, numLines is only the number of deleted lines, we +// ignore whether there are also some added lines in the hunk, as this is not +// relevant for our algorithm. +// +// - when the hunk contains only added lines, in which case (obviously) numLines +// is the number of added lines. +type hunk struct { filename string startLineIdx int numLines int @@ -37,17 +47,25 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { if err != nil { return err } - if diff == "" { + + deletedLineHunks, addedLineHunks := parseDiff(diff) + + var hashes []string + warnAboutAddedLines := false + + if len(deletedLineHunks) > 0 { + hashes, err = self.blameDeletedLines(deletedLineHunks) + warnAboutAddedLines = len(addedLineHunks) > 0 + } else if len(addedLineHunks) > 0 { + hashes, err = self.blameAddedLines(addedLineHunks) + } else { return errors.New(self.c.Tr.NoChangedFiles) } - deletedLineInfos, hasHunksWithOnlyAddedLines := self.parseDiff(diff) - if len(deletedLineInfos) == 0 { - return errors.New(self.c.Tr.NoDeletedLinesInDiff) + if err != nil { + return err } - hashes := self.blameDeletedLines(deletedLineInfos) - if len(hashes) == 0 { // This should never happen return errors.New(self.c.Tr.NoBaseCommitsFound) @@ -63,9 +81,7 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { return fmt.Errorf("%s\n\n%s", message, subjects) } - commit, index, ok := lo.FindIndexOf(self.c.Model().Commits, func(commit *models.Commit) bool { - return commit.Hash == hashes[0] - }) + commit, index, ok := self.findCommit(hashes[0]) if !ok { commits := self.c.Model().Commits if commits[len(commits)-1].Status == models.StatusMerged { @@ -93,7 +109,7 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { return self.c.PushContext(self.c.Contexts().LocalCommits) } - if hasHunksWithOnlyAddedLines { + if warnAboutAddedLines { return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.FindBaseCommitForFixup, Prompt: self.c.Tr.HunksWithOnlyAddedLinesWarning, @@ -122,29 +138,38 @@ func (self *FixupHelper) getDiff() (string, bool, error) { return diff, hasStagedChanges, err } -func (self *FixupHelper) parseDiff(diff string) ([]*deletedLineInfo, bool) { +// Parse the diff output into hunks, and return two lists of hunks: the first +// are ones that contain deleted lines, the second are ones that contain only +// added lines. +func parseDiff(diff string) ([]*hunk, []*hunk) { lines := strings.Split(strings.TrimSuffix(diff, "\n"), "\n") - deletedLineInfos := []*deletedLineInfo{} - hasHunksWithOnlyAddedLines := false + deletedLineHunks := []*hunk{} + addedLineHunks := []*hunk{} hunkHeaderRegexp := regexp.MustCompile(`@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@`) var filename string - var currentLineInfo *deletedLineInfo + var currentHunk *hunk + numDeletedLines := 0 + numAddedLines := 0 finishHunk := func() { - if currentLineInfo != nil { - if currentLineInfo.numLines > 0 { - deletedLineInfos = append(deletedLineInfos, currentLineInfo) - } else { - hasHunksWithOnlyAddedLines = true + if currentHunk != nil { + if numDeletedLines > 0 { + currentHunk.numLines = numDeletedLines + deletedLineHunks = append(deletedLineHunks, currentHunk) + } else if numAddedLines > 0 { + currentHunk.numLines = numAddedLines + addedLineHunks = append(addedLineHunks, currentHunk) } } + numDeletedLines = 0 + numAddedLines = 0 } for _, line := range lines { if strings.HasPrefix(line, "diff --git") { finishHunk() - currentLineInfo = nil + currentHunk = nil } else if strings.HasPrefix(line, "--- ") { // For some reason, the line ends with a tab character if the file // name contains spaces @@ -153,40 +178,42 @@ func (self *FixupHelper) parseDiff(diff string) ([]*deletedLineInfo, bool) { finishHunk() match := hunkHeaderRegexp.FindStringSubmatch(line) startIdx := utils.MustConvertToInt(match[1]) - currentLineInfo = &deletedLineInfo{filename, startIdx, 0} - } else if currentLineInfo != nil && line[0] == '-' { - currentLineInfo.numLines++ + currentHunk = &hunk{filename, startIdx, 0} + } else if currentHunk != nil && line[0] == '-' { + numDeletedLines++ + } else if currentHunk != nil && line[0] == '+' { + numAddedLines++ } } finishHunk() - return deletedLineInfos, hasHunksWithOnlyAddedLines + return deletedLineHunks, addedLineHunks } // returns the list of commit hashes that introduced the lines which have now been deleted -func (self *FixupHelper) blameDeletedLines(deletedLineInfos []*deletedLineInfo) []string { - var wg sync.WaitGroup +func (self *FixupHelper) blameDeletedLines(deletedLineHunks []*hunk) ([]string, error) { + errg := errgroup.Group{} hashChan := make(chan string) - for _, info := range deletedLineInfos { - wg.Add(1) - go func(info *deletedLineInfo) { - defer wg.Done() - - blameOutput, err := self.c.Git().Blame.BlameLineRange(info.filename, "HEAD", info.startLineIdx, info.numLines) + for _, h := range deletedLineHunks { + errg.Go(func() error { + blameOutput, err := self.c.Git().Blame.BlameLineRange(h.filename, "HEAD", h.startLineIdx, h.numLines) if err != nil { - self.c.Log.Errorf("Error blaming file '%s': %v", info.filename, err) - return + return err } blameLines := strings.Split(strings.TrimSuffix(blameOutput, "\n"), "\n") for _, line := range blameLines { hashChan <- strings.Split(line, " ")[0] } - }(info) + return nil + }) } go func() { - wg.Wait() + // We don't care about the error here, we'll check it later (in the + // return statement below). Here we only wait for all the goroutines to + // finish so that we can close the channel. + _ = errg.Wait() close(hashChan) }() @@ -195,5 +222,92 @@ func (self *FixupHelper) blameDeletedLines(deletedLineInfos []*deletedLineInfo) result.Add(hash) } - return result.ToSlice() + return result.ToSlice(), errg.Wait() +} + +func (self *FixupHelper) blameAddedLines(addedLineHunks []*hunk) ([]string, error) { + errg := errgroup.Group{} + hashesChan := make(chan []string) + + for _, h := range addedLineHunks { + errg.Go(func() error { + result := make([]string, 0, 2) + + appendBlamedLine := func(blameOutput string) { + blameLines := strings.Split(strings.T |