mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-11 08:06:25 -04:00
399 lines
12 KiB
Go
399 lines
12 KiB
Go
package context
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"slices"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type LocalCommitsContext struct {
|
|
*LocalCommitsViewModel
|
|
*ListContextTrait
|
|
*SearchTrait
|
|
|
|
dropIndicator *commitDropIndicator
|
|
}
|
|
|
|
type commitDropIndicator struct {
|
|
insertionIndex int
|
|
moving bool
|
|
}
|
|
|
|
var (
|
|
_ types.IListContext = (*LocalCommitsContext)(nil)
|
|
_ types.DiffableContext = (*LocalCommitsContext)(nil)
|
|
_ types.ISearchableContext = (*LocalCommitsContext)(nil)
|
|
_ types.DiffMainViewContext = (*LocalCommitsContext)(nil)
|
|
)
|
|
|
|
func (self *LocalCommitsContext) GetDiffMainViewType() types.DiffMainViewType {
|
|
return types.DiffMainViewTypePatchBuilding
|
|
}
|
|
|
|
func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
|
dropIndicator := &commitDropIndicator{insertionIndex: -1}
|
|
viewModel := NewLocalCommitsViewModel(
|
|
func() []*models.Commit { return c.Model().Commits },
|
|
c,
|
|
)
|
|
|
|
getDisplayStrings := func(startIdx int, endIdx int) [][]string {
|
|
var selectedCommitHashPtr *string
|
|
|
|
if c.Context().Current().GetKey() == LOCAL_COMMITS_CONTEXT_KEY {
|
|
selectedCommit := viewModel.GetSelected()
|
|
if selectedCommit != nil {
|
|
selectedCommitHashPtr = selectedCommit.HashPtr()
|
|
}
|
|
}
|
|
|
|
hasRebaseUpdateRefsConfig := c.Git().Config.GetRebaseUpdateRefs()
|
|
|
|
return presentation.GetCommitListDisplayStrings(
|
|
c.Common,
|
|
c.Model().Commits,
|
|
c.Model().Branches,
|
|
c.Model().CheckedOutBranch,
|
|
hasRebaseUpdateRefsConfig,
|
|
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
|
c.Modes().CherryPicking.SelectedHashSet(),
|
|
c.Modes().Diffing.Ref,
|
|
c.Modes().MarkedBaseCommit.GetHash(),
|
|
c.UserConfig().Gui.TimeFormat,
|
|
c.UserConfig().Gui.ShortTimeFormat,
|
|
time.Now(),
|
|
c.UserConfig().Git.ParseEmoji,
|
|
selectedCommitHashPtr,
|
|
startIdx,
|
|
endIdx,
|
|
shouldShowGraph(c),
|
|
c.Model().BisectInfo,
|
|
)
|
|
}
|
|
|
|
getNonModelItems := func() []*NonModelItem {
|
|
result := []*NonModelItem{}
|
|
if c.Model().WorkingTreeStateAtLastCommitRefresh.CanShowTodos() {
|
|
if c.Model().WorkingTreeStateAtLastCommitRefresh.Rebasing {
|
|
result = append(result, &NonModelItem{
|
|
Index: 0,
|
|
Content: formatListSectionHeader(c.Tr.PendingRebaseTodosSectionHeader),
|
|
})
|
|
}
|
|
|
|
if c.Model().WorkingTreeStateAtLastCommitRefresh.CherryPicking ||
|
|
c.Model().WorkingTreeStateAtLastCommitRefresh.Reverting {
|
|
_, firstCherryPickOrRevertTodo, found := lo.FindIndexOf(
|
|
c.Model().Commits, func(c *models.Commit) bool {
|
|
return c.Status == models.StatusCherryPickingOrReverting ||
|
|
c.Status == models.StatusConflicted
|
|
})
|
|
if !found {
|
|
firstCherryPickOrRevertTodo = 0
|
|
}
|
|
label := lo.Ternary(c.Model().WorkingTreeStateAtLastCommitRefresh.CherryPicking,
|
|
c.Tr.PendingCherryPicksSectionHeader,
|
|
c.Tr.PendingRevertsSectionHeader)
|
|
result = append(result, &NonModelItem{
|
|
Index: firstCherryPickOrRevertTodo,
|
|
Content: formatListSectionHeader(label),
|
|
})
|
|
}
|
|
|
|
result = addCommitDropIndicator(
|
|
result,
|
|
dropIndicator,
|
|
c.Tr.MoveCommitsHere,
|
|
c.Tr.MovingCommitsHere,
|
|
c.UserConfig().Gui.Spinner,
|
|
time.Now(),
|
|
)
|
|
|
|
_, firstRealCommit, found := lo.FindIndexOf(
|
|
c.Model().Commits, func(c *models.Commit) bool {
|
|
return !c.IsTODO()
|
|
})
|
|
if !found {
|
|
firstRealCommit = 0
|
|
}
|
|
result = append(result, &NonModelItem{
|
|
Index: firstRealCommit,
|
|
Content: formatListSectionHeader(c.Tr.CommitsSectionHeader),
|
|
})
|
|
} else {
|
|
result = addCommitDropIndicator(
|
|
result,
|
|
dropIndicator,
|
|
c.Tr.MoveCommitsHere,
|
|
c.Tr.MovingCommitsHere,
|
|
c.UserConfig().Gui.Spinner,
|
|
time.Now(),
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
ctx := &LocalCommitsContext{
|
|
LocalCommitsViewModel: viewModel,
|
|
SearchTrait: NewSearchTrait(c),
|
|
dropIndicator: dropIndicator,
|
|
ListContextTrait: &ListContextTrait{
|
|
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
|
View: c.Views().Commits,
|
|
WindowName: "commits",
|
|
Key: LOCAL_COMMITS_CONTEXT_KEY,
|
|
Kind: types.SIDE_CONTEXT,
|
|
Focusable: true,
|
|
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES,
|
|
NeedsRerenderOnHeightChange: true,
|
|
})),
|
|
ListRenderer: ListRenderer{
|
|
list: viewModel,
|
|
getDisplayStrings: getDisplayStrings,
|
|
getNonModelItems: getNonModelItems,
|
|
},
|
|
c: c,
|
|
refreshViewportOnChange: true,
|
|
renderOnlyVisibleLines: true,
|
|
},
|
|
}
|
|
|
|
return ctx
|
|
}
|
|
|
|
func addCommitDropIndicator(
|
|
items []*NonModelItem,
|
|
indicator *commitDropIndicator,
|
|
dropLabel string,
|
|
movingLabel string,
|
|
spinnerConfig config.SpinnerConfig,
|
|
now time.Time,
|
|
) []*NonModelItem {
|
|
if indicator.insertionIndex < 0 {
|
|
return items
|
|
}
|
|
label := dropLabel
|
|
if indicator.moving {
|
|
label = fmt.Sprintf("%s %s", movingLabel, presentation.Loader(now, spinnerConfig))
|
|
}
|
|
|
|
insertAt := len(items)
|
|
for i, item := range items {
|
|
if item.Index > indicator.insertionIndex {
|
|
insertAt = i
|
|
break
|
|
}
|
|
}
|
|
|
|
return slices.Insert(items, insertAt, &NonModelItem{
|
|
Index: indicator.insertionIndex,
|
|
Content: style.FgCyan.SetBold().Sprintf("━━━━━━ %s ━━━━━━", label),
|
|
Column: 6, // align with the commit subject
|
|
})
|
|
}
|
|
|
|
func (self *LocalCommitsContext) SetDropInsertionIndex(index int) {
|
|
self.dropIndicator.insertionIndex = index
|
|
self.dropIndicator.moving = false
|
|
}
|
|
|
|
func (self *LocalCommitsContext) SetMovingCommitsInsertionIndex(index int) {
|
|
self.dropIndicator.insertionIndex = index
|
|
self.dropIndicator.moving = true
|
|
}
|
|
|
|
func (self *LocalCommitsContext) ClearDropInsertionIndex() {
|
|
self.dropIndicator.insertionIndex = -1
|
|
self.dropIndicator.moving = false
|
|
}
|
|
|
|
type LocalCommitsViewModel struct {
|
|
*ListViewModel[*models.Commit]
|
|
|
|
// If this is true we limit the amount of commits we load, for the sake of keeping things fast.
|
|
// If the user attempts to scroll past the end of the list, we will load more commits.
|
|
// Atomic because a checkout or reset sets it from a worker goroutine while the
|
|
// commits refresh reads it on the UI thread to decide how many commits to load.
|
|
limitCommits atomic.Bool
|
|
|
|
// If this is true we'll use git log --all when fetching the commits.
|
|
showWholeGitGraph bool
|
|
}
|
|
|
|
func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel {
|
|
self := &LocalCommitsViewModel{
|
|
ListViewModel: NewListViewModel(getModel),
|
|
showWholeGitGraph: c.UserConfig().Git.Log.ShowWholeGraph,
|
|
}
|
|
self.limitCommits.Store(true)
|
|
|
|
return self
|
|
}
|
|
|
|
func (self *LocalCommitsContext) CanRebase() bool {
|
|
return true
|
|
}
|
|
|
|
func (self *LocalCommitsContext) GetSelectedRef() models.Ref {
|
|
commit := self.GetSelected()
|
|
if commit == nil {
|
|
return nil
|
|
}
|
|
return commit
|
|
}
|
|
|
|
func (self *LocalCommitsContext) GetSelectedRefRangeForDiffFiles() *types.RefRange {
|
|
commits, startIdx, endIdx := self.GetSelectedItems()
|
|
if commits == nil || startIdx == endIdx {
|
|
return nil
|
|
}
|
|
from := commits[len(commits)-1]
|
|
to := commits[0]
|
|
if from.IsTODO() || to.IsTODO() {
|
|
return nil
|
|
}
|
|
return &types.RefRange{From: from, To: to}
|
|
}
|
|
|
|
// Returns the commit hash of the selected commit, or an empty string if no
|
|
// commit is selected
|
|
func (self *LocalCommitsContext) GetSelectedCommitHash() string {
|
|
commit := self.GetSelected()
|
|
if commit == nil {
|
|
return ""
|
|
}
|
|
return commit.Hash()
|
|
}
|
|
|
|
func (self *LocalCommitsContext) SelectCommitByHash(hash string) bool {
|
|
if hash == "" {
|
|
return false
|
|
}
|
|
|
|
if _, idx, found := lo.FindIndexOf(self.GetItems(), func(c *models.Commit) bool { return c.Hash() == hash }); found {
|
|
self.SetSelection(idx)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (self *LocalCommitsContext) GetDiffTerminals() []string {
|
|
itemId := self.GetSelectedItemId()
|
|
|
|
return []string{itemId}
|
|
}
|
|
|
|
func (self *LocalCommitsContext) RefForAdjustingLineNumberInDiff() string {
|
|
commits, _, _ := self.GetSelectedItems()
|
|
if commits == nil {
|
|
return ""
|
|
}
|
|
return commits[0].Hash()
|
|
}
|
|
|
|
func (self *LocalCommitsContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
|
|
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.modelToViewIndexConverter(), searchStr)
|
|
}
|
|
|
|
func (self *LocalCommitsViewModel) SetLimitCommits(value bool) {
|
|
self.limitCommits.Store(value)
|
|
}
|
|
|
|
func (self *LocalCommitsViewModel) GetLimitCommits() bool {
|
|
return self.limitCommits.Load()
|
|
}
|
|
|
|
func (self *LocalCommitsViewModel) SetShowWholeGitGraph(value bool) {
|
|
self.showWholeGitGraph = value
|
|
}
|
|
|
|
func (self *LocalCommitsViewModel) GetShowWholeGitGraph() bool {
|
|
return self.showWholeGitGraph
|
|
}
|
|
|
|
func (self *LocalCommitsViewModel) GetCommits() []*models.Commit {
|
|
return self.getModel()
|
|
}
|
|
|
|
func shouldShowGraph(c *ContextCommon) bool {
|
|
// Whether we can draw a graph is a property of the commit list we have
|
|
// loaded, not of the filtering mode: turning filtering on or off only
|
|
// reaches the screen when the reloaded list does, and until then the graph
|
|
// has to keep matching the list that is still on display. Drawing one for a
|
|
// filtered list is also ruinously slow, because none of the commits in it
|
|
// are connected to each other, so no pipe ever terminates.
|
|
if c.Model().CommitsWereFilteredAtLastRefresh {
|
|
return false
|
|
}
|
|
|
|
value := c.UserConfig().Git.Log.ShowGraph
|
|
|
|
switch value {
|
|
case "always":
|
|
return true
|
|
case "never":
|
|
return false
|
|
case "when-maximised":
|
|
return c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL
|
|
}
|
|
|
|
log.Fatalf("Unknown value for git.log.showGraph: %s. Expected one of: 'always', 'never', 'when-maximised'", value)
|
|
return false
|
|
}
|
|
|
|
func searchModelCommits(caseSensitive bool, commits []*models.Commit, columnPositions []int,
|
|
modelToViewIndex func(int) int, searchStr string,
|
|
) []gocui.SearchPosition {
|
|
if columnPositions == nil {
|
|
// This should never happen. We are being called at a time where our
|
|
// entire view content is scrolled out of view, so that we didn't draw
|
|
// anything the last time we rendered. If we run into a scenario where
|
|
// this happens, we should fix it, but until we found them all, at least
|
|
// make sure we don't crash.
|
|
return []gocui.SearchPosition{}
|
|
}
|
|
|
|
normalize := lo.Ternary(caseSensitive, func(s string) string { return s }, strings.ToLower)
|
|
return lo.FilterMap(commits, func(commit *models.Commit, idx int) (gocui.SearchPosition, bool) {
|
|
// The XStart and XEnd values are only used if the search string can't
|
|
// be found in the view. This can really only happen if the user is
|
|
// searching for a commit hash that is longer than the truncated hash
|
|
// that we render. So we just set the XStart and XEnd values to the
|
|
// start and end of the commit hash column, which is the second one.
|
|
result := gocui.SearchPosition{XStart: columnPositions[1], XEnd: columnPositions[2] - 1, Y: modelToViewIndex(idx)}
|
|
return result, strings.Contains(normalize(commit.Hash()), searchStr) ||
|
|
strings.Contains(normalize(commit.Name), searchStr) ||
|
|
strings.Contains(normalize(commit.ExtraInfo), searchStr) // allow searching for tags
|
|
})
|
|
}
|
|
|
|
func (self *LocalCommitsContext) IndexForGotoBottom() int {
|
|
commits := self.GetCommits()
|
|
selectedIdx := self.GetSelectedLineIdx()
|
|
if selectedIdx >= 0 && selectedIdx < len(commits)-1 {
|
|
if commits[selectedIdx+1].Status != models.StatusMerged {
|
|
_, idx, found := lo.FindIndexOf(commits, func(c *models.Commit) bool {
|
|
return c.Status == models.StatusMerged
|
|
})
|
|
if found {
|
|
return idx - 1
|
|
}
|
|
}
|
|
}
|
|
|
|
return self.list.Len() - 1
|
|
}
|