Compare commits
2 Commits
Add-root-n
...
fast-ances
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e69a04680b | ||
|
|
a2a8755a7c |
@@ -45,6 +45,7 @@ type Loaders struct {
|
||||
BranchLoader *git_commands.BranchLoader
|
||||
CommitFileLoader *git_commands.CommitFileLoader
|
||||
CommitLoader *git_commands.CommitLoader
|
||||
CommitStoreLoader *git_commands.CommitStoreLoader
|
||||
FileLoader *git_commands.FileLoader
|
||||
ReflogCommitLoader *git_commands.ReflogCommitLoader
|
||||
RemoteLoader *git_commands.RemoteLoader
|
||||
@@ -131,6 +132,7 @@ func NewGitCommandAux(
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode)
|
||||
commitStoreLoader := git_commands.NewCommitStoreLoader(cmn, cmd)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
|
||||
stashLoader := git_commands.NewStashLoader(cmn, cmd)
|
||||
@@ -158,6 +160,7 @@ func NewGitCommandAux(
|
||||
BranchLoader: branchLoader,
|
||||
CommitFileLoader: commitFileLoader,
|
||||
CommitLoader: commitLoader,
|
||||
CommitStoreLoader: commitStoreLoader,
|
||||
FileLoader: fileLoader,
|
||||
ReflogCommitLoader: reflogCommitLoader,
|
||||
RemoteLoader: remoteLoader,
|
||||
|
||||
@@ -165,7 +165,7 @@ var branchFields = []string{
|
||||
"upstream:short",
|
||||
"upstream:track",
|
||||
"subject",
|
||||
fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE),
|
||||
"objectname",
|
||||
}
|
||||
|
||||
// Obtain branch information from parsed line output of getRawBranches()
|
||||
|
||||
65
pkg/commands/git_commands/commit_store_loader.go
Normal file
65
pkg/commands/git_commands/commit_store_loader.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
// CommitStoreLoader populates a commit store with commits from the git log.
|
||||
type CommitStoreLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewCommitStoreLoader(
|
||||
cmn *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *CommitStoreLoader {
|
||||
return &CommitStoreLoader{
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
// mutates the given commit store to add commits from the git log
|
||||
func (self *CommitStoreLoader) Load(commitStore *models.CommitStore) error {
|
||||
t := time.Now()
|
||||
|
||||
err := self.getLogCmd().RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit := self.extractCommitFromLine(line)
|
||||
commitStore.Add(commit)
|
||||
return false, nil
|
||||
})
|
||||
|
||||
self.Log.Warnf("CommitStoreLoader Load took %s", time.Since(t))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// getLog gets the git log.
|
||||
func (self *CommitStoreLoader) getLogCmd() oscommands.ICmdObj {
|
||||
cmdArgs := NewGitCmd("log").
|
||||
Arg("--all").
|
||||
Arg(`--pretty=format:%H%x00%P`).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
func (self *CommitStoreLoader) extractCommitFromLine(line string) models.ImmutableCommit {
|
||||
split := strings.SplitN(line, "\x00", 2)
|
||||
|
||||
sha := split[0]
|
||||
|
||||
parentsStr := split[1]
|
||||
parents := []string{}
|
||||
if len(parentsStr) > 0 {
|
||||
parents = strings.Split(parentsStr, " ")
|
||||
}
|
||||
|
||||
return models.NewImmutableCommit(sha, parents)
|
||||
}
|
||||
99
pkg/commands/git_commands/commit_store_loader_test.go
Normal file
99
pkg/commands/git_commands/commit_store_loader_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommitStoreLoaderLoad(t *testing.T) {
|
||||
commitsOutputStore := strings.Replace(`a|b
|
||||
b|c d
|
||||
c|e
|
||||
d|e
|
||||
e|`, "|", "\x00", -1)
|
||||
|
||||
args := []string{"log", "--all", "--pretty=format:%H%x00%P"}
|
||||
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
startingCommits []models.ImmutableCommit
|
||||
expectedCommits []models.ImmutableCommit
|
||||
expectedError error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no commits if there are none",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(args, "", nil),
|
||||
|
||||
startingCommits: []models.ImmutableCommit{},
|
||||
expectedCommits: []models.ImmutableCommit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "properly processes commits",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(args, commitsOutputStore, nil),
|
||||
|
||||
startingCommits: []models.ImmutableCommit{},
|
||||
expectedCommits: []models.ImmutableCommit{
|
||||
models.NewImmutableCommit("a", []string{"b"}),
|
||||
models.NewImmutableCommit("b", []string{"c", "d"}),
|
||||
models.NewImmutableCommit("c", []string{"e"}),
|
||||
models.NewImmutableCommit("d", []string{"e"}),
|
||||
models.NewImmutableCommit("e", []string{}),
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "merges into exising commits",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(args, commitsOutputStore, nil),
|
||||
|
||||
startingCommits: []models.ImmutableCommit{
|
||||
// this one is present in the command output
|
||||
models.NewImmutableCommit("a", []string{"b"}),
|
||||
// this one isn't
|
||||
models.NewImmutableCommit("f", []string{"g"}),
|
||||
},
|
||||
expectedCommits: []models.ImmutableCommit{
|
||||
models.NewImmutableCommit("a", []string{"b"}),
|
||||
models.NewImmutableCommit("b", []string{"c", "d"}),
|
||||
models.NewImmutableCommit("c", []string{"e"}),
|
||||
models.NewImmutableCommit("d", []string{"e"}),
|
||||
models.NewImmutableCommit("e", []string{}),
|
||||
models.NewImmutableCommit("f", []string{"g"}),
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
common := utils.NewDummyCommon()
|
||||
|
||||
builder := &CommitStoreLoader{
|
||||
Common: common,
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
}
|
||||
|
||||
commitStore := models.NewCommitStore()
|
||||
commitStore.AddSlice(scenario.startingCommits)
|
||||
|
||||
err := builder.Load(commitStore)
|
||||
|
||||
assert.EqualValues(t, scenario.expectedCommits, commitStore.Slice())
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ const (
|
||||
StatusPushed
|
||||
StatusMerged
|
||||
StatusRebasing
|
||||
StatusSelected
|
||||
StatusReflog
|
||||
)
|
||||
|
||||
@@ -30,18 +29,21 @@ const (
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
// TODO: rename to hash
|
||||
Sha string
|
||||
// Commit message subject line
|
||||
Name string
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
AuthorName string // something like 'Jesse Duffield'
|
||||
AuthorEmail string // something like 'jessedduffield@gmail.com'
|
||||
UnixTimestamp int64
|
||||
|
||||
// SHAs of parent commits (will be multiple if it's a merge commit)
|
||||
Parents []string
|
||||
|
||||
// mutable fields
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
}
|
||||
|
||||
func (c *Commit) ShortSha() string {
|
||||
|
||||
164
pkg/commands/models/commit_store.go
Normal file
164
pkg/commands/models/commit_store.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// This stores immutable commits as a graph for fast lookup
|
||||
type CommitStore struct {
|
||||
commitsMap map[string]ImmutableCommit
|
||||
mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
func NewCommitStore() *CommitStore {
|
||||
return &CommitStore{commitsMap: make(map[string]ImmutableCommit), mutex: &sync.RWMutex{}}
|
||||
}
|
||||
|
||||
func (self *CommitStore) Add(commit ImmutableCommit) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
self.commitsMap[commit.Hash()] = commit
|
||||
}
|
||||
|
||||
func (self *CommitStore) AddSlice(commit []ImmutableCommit) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
for _, commit := range commit {
|
||||
self.commitsMap[commit.Hash()] = commit
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitStore) GetCommit(hash string) (ImmutableCommit, bool) {
|
||||
self.mutex.RLock()
|
||||
defer self.mutex.RUnlock()
|
||||
|
||||
commit, ok := self.commitsMap[hash]
|
||||
return commit, ok
|
||||
}
|
||||
|
||||
func (self *CommitStore) GetParents(hash string) ([]ImmutableCommit, bool) {
|
||||
commit, ok := self.GetCommit(hash)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
parents := make([]ImmutableCommit, len(commit.ParentHashes()))
|
||||
for i, parentHash := range commit.ParentHashes() {
|
||||
parents[i], ok = self.GetCommit(parentHash)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
return parents, true
|
||||
}
|
||||
|
||||
type IsAncestorResponse int
|
||||
|
||||
const (
|
||||
IsAncestorResponseYes IsAncestorResponse = iota
|
||||
IsAncestorResponseNo
|
||||
// returned when we can't find the commit, or if one of the ancestors along the chain can't be found
|
||||
IsAncestorResponseUnknown
|
||||
)
|
||||
|
||||
var IsAncestorResponseStrings = []string{
|
||||
"yes",
|
||||
"no",
|
||||
"unknown",
|
||||
}
|
||||
|
||||
func (self *CommitStore) IsAncestor(hash string, ancestorHash string) IsAncestorResponse {
|
||||
if hash == ancestorHash {
|
||||
return IsAncestorResponseYes
|
||||
}
|
||||
|
||||
commit, ok := self.GetCommit(hash)
|
||||
if !ok {
|
||||
return IsAncestorResponseUnknown
|
||||
}
|
||||
|
||||
if commit.IsRoot() {
|
||||
return IsAncestorResponseNo
|
||||
}
|
||||
|
||||
parentHashes := commit.ParentHashes()
|
||||
|
||||
// first check the parent hashes themselves: spares us attempting a lookup of the actual parent structs
|
||||
for _, parentHash := range parentHashes {
|
||||
if parentHash == ancestorHash {
|
||||
return IsAncestorResponseYes
|
||||
}
|
||||
}
|
||||
|
||||
unknown := false
|
||||
for _, parentHash := range parentHashes {
|
||||
response := self.IsAncestor(parentHash, ancestorHash)
|
||||
if response == IsAncestorResponseYes {
|
||||
return IsAncestorResponseYes
|
||||
}
|
||||
if response == IsAncestorResponseUnknown {
|
||||
unknown = true
|
||||
}
|
||||
}
|
||||
|
||||
if unknown {
|
||||
return IsAncestorResponseUnknown
|
||||
}
|
||||
|
||||
return IsAncestorResponseNo
|
||||
}
|
||||
|
||||
// used for testing
|
||||
func (self *CommitStore) Slice() []ImmutableCommit {
|
||||
self.mutex.RLock()
|
||||
defer self.mutex.RUnlock()
|
||||
|
||||
commits := lo.Values(self.commitsMap)
|
||||
|
||||
// sort by hash for deterministic result
|
||||
slices.SortFunc(commits, func(a, b ImmutableCommit) bool {
|
||||
return a.Hash() < b.Hash()
|
||||
})
|
||||
|
||||
return commits
|
||||
}
|
||||
|
||||
func (self *CommitStore) Size() int {
|
||||
return len(self.commitsMap)
|
||||
}
|
||||
|
||||
func (self *CommitStore) dfs(hash string, visited map[string]bool) {
|
||||
visited[hash] = true
|
||||
commit, ok := self.GetCommit(hash)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Traverse the parent hashes
|
||||
for _, parentHash := range commit.ParentHashes() {
|
||||
if !visited[parentHash] {
|
||||
self.dfs(parentHash, visited)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns subset of candidates which are ancestors of the given hash
|
||||
func (self *CommitStore) FindAncestors(hash string, candidates []string) map[string]bool {
|
||||
visited := make(map[string]bool)
|
||||
self.dfs(hash, visited)
|
||||
|
||||
ancestors := map[string]bool{}
|
||||
for _, commitHash := range candidates {
|
||||
if visited[commitHash] {
|
||||
ancestors[commitHash] = true
|
||||
}
|
||||
}
|
||||
|
||||
return ancestors
|
||||
}
|
||||
88
pkg/commands/models/commit_store_test.go
Normal file
88
pkg/commands/models/commit_store_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommitStore(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
commits []ImmutableCommit
|
||||
hash string
|
||||
ancestorHash string
|
||||
expected IsAncestorResponse
|
||||
}{
|
||||
{
|
||||
testName: "Empty commit store, same hash",
|
||||
commits: []ImmutableCommit{},
|
||||
hash: "a",
|
||||
ancestorHash: "a",
|
||||
expected: IsAncestorResponseYes,
|
||||
},
|
||||
{
|
||||
testName: "Empty commit store, different hash",
|
||||
commits: []ImmutableCommit{},
|
||||
hash: "a",
|
||||
ancestorHash: "b",
|
||||
expected: IsAncestorResponseUnknown,
|
||||
},
|
||||
{
|
||||
testName: "Hash found, ancestor not found",
|
||||
commits: []ImmutableCommit{
|
||||
NewImmutableCommit("a", []string{"c"}),
|
||||
NewImmutableCommit("c", []string{}),
|
||||
},
|
||||
hash: "a",
|
||||
ancestorHash: "b",
|
||||
expected: IsAncestorResponseNo,
|
||||
},
|
||||
{
|
||||
testName: "Hash found, ancestor is parent",
|
||||
commits: []ImmutableCommit{
|
||||
NewImmutableCommit("a", []string{"b"}),
|
||||
NewImmutableCommit("b", []string{"c"}),
|
||||
},
|
||||
hash: "a",
|
||||
ancestorHash: "b",
|
||||
expected: IsAncestorResponseYes,
|
||||
},
|
||||
{
|
||||
testName: "Hash found, ancestor is grandparent",
|
||||
commits: []ImmutableCommit{
|
||||
NewImmutableCommit("a", []string{"b"}),
|
||||
NewImmutableCommit("b", []string{"c"}),
|
||||
},
|
||||
hash: "a",
|
||||
ancestorHash: "c",
|
||||
expected: IsAncestorResponseYes,
|
||||
},
|
||||
{
|
||||
testName: "Hash found, not an ancestor",
|
||||
commits: []ImmutableCommit{
|
||||
NewImmutableCommit("a", []string{"b"}),
|
||||
NewImmutableCommit("b", []string{"d"}),
|
||||
NewImmutableCommit("c", []string{"d"}),
|
||||
NewImmutableCommit("d", []string{}),
|
||||
},
|
||||
hash: "a",
|
||||
ancestorHash: "c",
|
||||
expected: IsAncestorResponseNo,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
commitStore := NewCommitStore()
|
||||
commitStore.AddSlice(scenario.commits)
|
||||
|
||||
response := commitStore.IsAncestor(scenario.hash, scenario.ancestorHash)
|
||||
|
||||
if response != scenario.expected {
|
||||
responseStr := IsAncestorResponseStrings[response]
|
||||
expectedStr := IsAncestorResponseStrings[scenario.expected]
|
||||
|
||||
t.Errorf("Expected %s, got %s", expectedStr, responseStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
30
pkg/commands/models/immutable_commit.go
Normal file
30
pkg/commands/models/immutable_commit.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package models
|
||||
|
||||
// This model contains the information that is intrinsic to a commit, meaning
|
||||
// that we can depend on it not changing over time. Git commits are immutable,
|
||||
// but our other Commit model has extra fields added for ease of use.
|
||||
type ImmutableCommit struct {
|
||||
hash string
|
||||
|
||||
// hashes of parent commits (will be multiple if it's a merge commit)
|
||||
parentHashes []string
|
||||
}
|
||||
|
||||
func NewImmutableCommit(hash string, parentHashes []string) ImmutableCommit {
|
||||
return ImmutableCommit{
|
||||
hash: hash,
|
||||
parentHashes: parentHashes,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *ImmutableCommit) Hash() string {
|
||||
return self.hash
|
||||
}
|
||||
|
||||
func (self *ImmutableCommit) ParentHashes() []string {
|
||||
return self.parentHashes
|
||||
}
|
||||
|
||||
func (self *ImmutableCommit) IsRoot() bool {
|
||||
return len(self.parentHashes) == 0
|
||||
}
|
||||
@@ -26,6 +26,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
||||
c.Modes().Diffing.Ref,
|
||||
c.Tr,
|
||||
c.UserConfig,
|
||||
c.Model().CommitStore,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
|
||||
// e.g. in the case of switching branches.
|
||||
func (self *RefreshHelper) refreshCommits() {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
wg.Add(3)
|
||||
|
||||
go utils.Safe(func() {
|
||||
self.refreshReflogCommitsConsideringStartup()
|
||||
@@ -231,6 +231,12 @@ func (self *RefreshHelper) refreshCommits() {
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
go utils.Safe(func() {
|
||||
self.refreshCommitStore()
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
go utils.Safe(func() {
|
||||
_ = self.refreshCommitsWithLimit()
|
||||
ctx, ok := self.c.Contexts().CommitFiles.GetParentContext()
|
||||
@@ -254,6 +260,16 @@ func (self *RefreshHelper) refreshCommits() {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshCommitStore() {
|
||||
self.c.Mutexes().CommitStoreMutex.Lock()
|
||||
defer self.c.Mutexes().CommitStoreMutex.Unlock()
|
||||
|
||||
err := self.c.Git().Loaders.CommitStoreLoader.Load(self.c.Model().CommitStore)
|
||||
if err != nil {
|
||||
_ = self.c.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshCommitsWithLimit() error {
|
||||
self.c.Mutexes().LocalCommitsMutex.Lock()
|
||||
defer self.c.Mutexes().LocalCommitsMutex.Unlock()
|
||||
|
||||
@@ -342,6 +342,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.
|
||||
CommitFiles: nil,
|
||||
Files: make([]*models.File, 0),
|
||||
Commits: make([]*models.Commit, 0),
|
||||
CommitStore: models.NewCommitStore(),
|
||||
StashEntries: make([]*models.StashEntry, 0),
|
||||
FilteredReflogCommits: make([]*models.Commit, 0),
|
||||
ReflogCommits: make([]*models.Commit, 0),
|
||||
@@ -451,6 +452,7 @@ func NewGui(
|
||||
RefreshingStatusMutex: &deadlock.Mutex{},
|
||||
SyncMutex: &deadlock.Mutex{},
|
||||
LocalCommitsMutex: &deadlock.Mutex{},
|
||||
CommitStoreMutex: &deadlock.Mutex{},
|
||||
SubCommitsMutex: &deadlock.Mutex{},
|
||||
SubprocessMutex: &deadlock.Mutex{},
|
||||
PopupMutex: &deadlock.Mutex{},
|
||||
|
||||
@@ -3,6 +3,7 @@ package presentation
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -10,8 +11,10 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var branchPrefixColorCache = make(map[string]style.TextStyle)
|
||||
@@ -22,10 +25,21 @@ func GetBranchListDisplayStrings(
|
||||
diffName string,
|
||||
tr *i18n.TranslationSet,
|
||||
userConfig *config.UserConfig,
|
||||
commitStore *models.CommitStore,
|
||||
) [][]string {
|
||||
isContainedInMainBranch := isContainedInMainBranchFn(
|
||||
userConfig.Git.MainBranches,
|
||||
branches,
|
||||
commitStore,
|
||||
)
|
||||
|
||||
if commitStore.Size() > 10 {
|
||||
logs.Global.Warnf("commitStore.Slice()[0:10]: %v", commitStore.Slice()[0:10])
|
||||
}
|
||||
|
||||
return slices.Map(branches, func(branch *models.Branch) []string {
|
||||
diffed := branch.Name == diffName
|
||||
return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig)
|
||||
return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig, isContainedInMainBranch)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,6 +50,7 @@ func getBranchDisplayStrings(
|
||||
diffed bool,
|
||||
tr *i18n.TranslationSet,
|
||||
userConfig *config.UserConfig,
|
||||
isContainedInMainBranch func(string) bool,
|
||||
) []string {
|
||||
displayName := b.Name
|
||||
if b.DisplayName != "" {
|
||||
@@ -63,7 +78,15 @@ func getBranchDisplayStrings(
|
||||
}
|
||||
|
||||
if fullDescription || userConfig.Gui.ShowBranchCommitHash {
|
||||
res = append(res, b.CommitHash)
|
||||
var hashStyle style.TextStyle
|
||||
if isContainedInMainBranch(b.CommitHash) {
|
||||
hashStyle = style.FgGreen
|
||||
} else {
|
||||
hashStyle = style.FgYellow
|
||||
}
|
||||
coloredHash := hashStyle.Sprint(utils.ShortSha(b.CommitHash))
|
||||
|
||||
res = append(res, coloredHash)
|
||||
}
|
||||
|
||||
res = append(res, coloredName)
|
||||
@@ -144,3 +167,47 @@ func BranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string {
|
||||
func SetCustomBranches(customBranchColors map[string]string) {
|
||||
branchPrefixColorCache = utils.SetCustomColors(customBranchColors)
|
||||
}
|
||||
|
||||
// returns a function that tells us if a given branch's commit is contained in any of the main branches
|
||||
func isContainedInMainBranchFn(mainBranches []string, branches []*models.Branch, commitStore *models.CommitStore) func(string) bool {
|
||||
mainBranchHashes := []string{}
|
||||
for _, branch := range branches {
|
||||
if lo.Contains(mainBranches, branch.Name) {
|
||||
mainBranchHashes = append(mainBranchHashes, branch.CommitHash)
|
||||
}
|
||||
}
|
||||
|
||||
logs.Global.Warnf("mainBranchHashes: %v", mainBranchHashes)
|
||||
|
||||
t := time.Now()
|
||||
|
||||
ancestorSlices := lo.Map(mainBranchHashes, func(hash string, _ int) map[string]bool {
|
||||
return commitStore.FindAncestors(
|
||||
hash,
|
||||
lo.Map(branches, func(branch *models.Branch, _ int) string {
|
||||
return branch.CommitHash
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
ancestors := lo.Reduce(ancestorSlices, mergeMaps, map[string]bool{})
|
||||
|
||||
logs.Global.Warnf("isContainedInMainBranchFn took %v", time.Since(t))
|
||||
|
||||
return func(commitHash string) bool {
|
||||
return ancestors[commitHash]
|
||||
}
|
||||
}
|
||||
|
||||
func mergeMaps(a, b map[string]bool, _ int) map[string]bool {
|
||||
res := map[string]bool{}
|
||||
for k, v := range a {
|
||||
res[k] = v
|
||||
}
|
||||
for k, v := range b {
|
||||
res[k] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// it's faster to first check if master is a proper ancestor of my commit, because it likely is and it will be faster to find out. If I go the other way around, I need to traverse from master to the root for every branch potentially. Is there a way to speed that up?
|
||||
|
||||
@@ -192,6 +192,7 @@ type Model struct {
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
Commits []*models.Commit
|
||||
CommitStore *models.CommitStore
|
||||
StashEntries []*models.StashEntry
|
||||
SubCommits []*models.Commit
|
||||
Remotes []*models.Remote
|
||||
@@ -220,6 +221,7 @@ type Mutexes struct {
|
||||
RefreshingStatusMutex *deadlock.Mutex
|
||||
SyncMutex *deadlock.Mutex
|
||||
LocalCommitsMutex *deadlock.Mutex
|
||||
CommitStoreMutex *deadlock.Mutex
|
||||
SubCommitsMutex *deadlock.Mutex
|
||||
SubprocessMutex *deadlock.Mutex
|
||||
PopupMutex *deadlock.Mutex
|
||||
|
||||
Reference in New Issue
Block a user