Compare commits
4 Commits
create-pul
...
show-merge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
017367f940 | ||
|
|
16369dc3c2 | ||
|
|
86bb73cff1 | ||
|
|
bac12231c3 |
10
README.md
10
README.md
@@ -478,9 +478,13 @@ If you would like to support the development of lazygit, consider [sponsoring me
|
||||
|
||||
### What do the commit colors represent?
|
||||
|
||||
- Green: the commit is included in the master branch
|
||||
- Yellow: the commit is not included in the master branch
|
||||
- Red: the commit has not been pushed to the upstream branch
|
||||
- Green: the commit is included in the main branch's upstream
|
||||
- Yellow: the commit is not included in the main branch's upstream
|
||||
- Red: the commit has not been pushed to the given branch's upstream
|
||||
|
||||
By 'main' branch we mean any branch defined in the `git.mainBranches` config which defaults to 'master' and 'main'. We check against the upstream and if that's missing we fall back to checking against the local branch itself. We prioritise the upstream because it's typically more up-to-date.
|
||||
|
||||
This colouring scheme lets you see at a glance which commits belong to your branch, and which commits belong to the main branch.
|
||||
|
||||
## Shameless Plug
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ type GitCommand struct {
|
||||
|
||||
type Loaders struct {
|
||||
BranchLoader *git_commands.BranchLoader
|
||||
MergedBranchLoader *git_commands.MergedBranchLoader
|
||||
CommitFileLoader *git_commands.CommitFileLoader
|
||||
CommitLoader *git_commands.CommitLoader
|
||||
FileLoader *git_commands.FileLoader
|
||||
@@ -162,6 +163,7 @@ func NewGitCommandAux(
|
||||
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
mergedBranchLoader := git_commands.NewMergedBranchLoader(gitCommon)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
@@ -200,6 +202,7 @@ func NewGitCommandAux(
|
||||
Worktrees: worktreeLoader,
|
||||
StashLoader: stashLoader,
|
||||
TagLoader: tagLoader,
|
||||
MergedBranchLoader: mergedBranchLoader,
|
||||
},
|
||||
RepoPaths: repoPaths,
|
||||
}
|
||||
|
||||
@@ -568,50 +568,26 @@ func (self *CommitLoader) getMergeBase(refName string) string {
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getExistingMainBranches() []string {
|
||||
var existingBranches []string
|
||||
var wg sync.WaitGroup
|
||||
|
||||
mainBranches := self.UserConfig.Git.MainBranches
|
||||
existingBranches = make([]string, len(mainBranches))
|
||||
|
||||
for i, branchName := range mainBranches {
|
||||
wg.Add(1)
|
||||
i := i
|
||||
branchName := branchName
|
||||
go utils.Safe(func() {
|
||||
defer wg.Done()
|
||||
existingBranches := utils.ConcurrentMap(mainBranches, func(mainBranch string) string {
|
||||
// Try to determine upstream of local main branch
|
||||
if ref, err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--symbolic-full-name", mainBranch+"@{u}").ToArgv(),
|
||||
).DontLog().RunWithOutput(); err == nil {
|
||||
return strings.TrimSpace(ref)
|
||||
}
|
||||
|
||||
// Try to determine upstream of local main branch
|
||||
if ref, err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--symbolic-full-name", branchName+"@{u}").ToArgv(),
|
||||
).DontLog().RunWithOutput(); err == nil {
|
||||
existingBranches[i] = strings.TrimSpace(ref)
|
||||
return
|
||||
}
|
||||
// If this failed, fallback to the local branch
|
||||
ref := "refs/heads/" + mainBranch
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
||||
).DontLog().Run(); err == nil {
|
||||
return ref
|
||||
}
|
||||
|
||||
// If this failed, a local branch for this main branch doesn't exist or it
|
||||
// has no upstream configured. Try looking for one in the "origin" remote.
|
||||
ref := "refs/remotes/origin/" + branchName
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
||||
).DontLog().Run(); err == nil {
|
||||
existingBranches[i] = ref
|
||||
return
|
||||
}
|
||||
|
||||
// If this failed as well, try if we have the main branch as a local
|
||||
// branch. This covers the case where somebody is using git locally
|
||||
// for something, but never pushing anywhere.
|
||||
ref = "refs/heads/" + branchName
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--verify", "--quiet", ref).ToArgv(),
|
||||
).DontLog().Run(); err == nil {
|
||||
existingBranches[i] = ref
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return ""
|
||||
})
|
||||
|
||||
existingBranches = lo.Filter(existingBranches, func(branch string, _ int) bool {
|
||||
return branch != ""
|
||||
|
||||
@@ -74,12 +74,10 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
|
||||
// here it's testing which of the configured main branches have an upstream
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/main"}, "", nil). // yep, origin/main exists
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "develop@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/develop"}, "", errors.New("error")). // doesn't exist there, either, so it checks for a local branch
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/develop"}, "", errors.New("error")). // no local branch either
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "refs/remotes/origin/main", nil). // this one does too
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "develop@{u}"}, "", errors.New("error")). // this one doesn't, so check for a local branch
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/develop"}, "", errors.New("error")). // no local branch either
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "refs/remotes/origin/master", "refs/remotes/origin/main"}, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
@@ -212,10 +210,8 @@ func TestGetCommits(t *testing.T) {
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s%x00%m", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist; neither does
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/master"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/master"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/main"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/main"}, "", errors.New("error")),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
@@ -250,7 +246,6 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's testing which of the configured main branches exist
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/main"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/main"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "develop@{u}"}, "refs/remotes/origin/develop", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "1.0-hotfixes@{u}"}, "refs/remotes/origin/1.0-hotfixes", nil).
|
||||
|
||||
59
pkg/commands/git_commands/merged_branch_loader.go
Normal file
59
pkg/commands/git_commands/merged_branch_loader.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Returns a set of branch names which are merged into a main branch.
|
||||
// This
|
||||
type MergedBranchLoader struct {
|
||||
c *GitCommon
|
||||
}
|
||||
|
||||
func NewMergedBranchLoader(c *GitCommon) *MergedBranchLoader {
|
||||
return &MergedBranchLoader{c: c}
|
||||
}
|
||||
|
||||
// TODO: check against upstreams, and share code with commit loader that determines main branches
|
||||
func (self *MergedBranchLoader) Load() *set.Set[string] {
|
||||
set := set.New[string]()
|
||||
|
||||
mainBranches := self.c.UserConfig.Git.MainBranches
|
||||
results := utils.ConcurrentMap(mainBranches, func(mainBranch string) []string {
|
||||
mergedBranches, err := self.GetMergedBranches(mainBranch)
|
||||
if err != nil {
|
||||
self.c.Log.Warnf("Failed to get merged branches for %s: %s", mainBranch, err)
|
||||
return nil
|
||||
}
|
||||
return mergedBranches
|
||||
})
|
||||
|
||||
for i := 0; i < len(mainBranches); i++ {
|
||||
for _, mergedBranch := range results[i] {
|
||||
set.Add(mergedBranch)
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (self *MergedBranchLoader) GetMergedBranches(mainBranch string) ([]string, error) {
|
||||
// git for-each-ref --merged master --format '%(refname)' refs/heads/
|
||||
cmdArgs := NewGitCmd("for-each-ref").Arg(
|
||||
"--merged",
|
||||
mainBranch,
|
||||
"--format",
|
||||
"%(refname)",
|
||||
"refs/heads/",
|
||||
).ToArgv()
|
||||
|
||||
output, err := self.c.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branches := utils.SplitLines(output)
|
||||
|
||||
return branches, nil
|
||||
}
|
||||
@@ -33,6 +33,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
||||
c.Tr,
|
||||
c.UserConfig,
|
||||
c.Model().Worktrees,
|
||||
c.Model().MergedBranches,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -429,25 +429,43 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool) {
|
||||
self.c.Mutexes().RefreshingBranchesMutex.Lock()
|
||||
defer self.c.Mutexes().RefreshingBranchesMutex.Unlock()
|
||||
|
||||
reflogCommits := self.c.Model().FilteredReflogCommits
|
||||
if self.c.Modes().Filtering.Active() {
|
||||
// in filter mode we filter our reflog commits to just those containing the path
|
||||
// however we need all the reflog entries to populate the recencies of our branches
|
||||
// which allows us to order them correctly. So if we're filtering we'll just
|
||||
// manually load all the reflog commits here
|
||||
var err error
|
||||
reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(nil, "")
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
reflogCommits := self.c.Model().FilteredReflogCommits
|
||||
if self.c.Modes().Filtering.Active() {
|
||||
// in filter mode we filter our reflog commits to just those containing the path
|
||||
// however we need all the reflog entries to populate the recencies of our branches
|
||||
// which allows us to order them correctly. So if we're filtering we'll just
|
||||
// manually load all the reflog commits here
|
||||
var err error
|
||||
reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(nil, "")
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
branches, err := self.c.Git().Loaders.BranchLoader.Load(reflogCommits)
|
||||
if err != nil {
|
||||
_ = self.c.Error(err)
|
||||
}
|
||||
branches, err := self.c.Git().Loaders.BranchLoader.Load(reflogCommits)
|
||||
if err != nil {
|
||||
_ = self.c.Error(err)
|
||||
}
|
||||
|
||||
self.c.Model().Branches = branches
|
||||
self.c.Model().Branches = branches
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
mergedBranches := self.c.Git().Loaders.MergedBranchLoader.Load()
|
||||
|
||||
self.c.Model().MergedBranches = mergedBranches
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if refreshWorktrees {
|
||||
self.loadWorktrees()
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazycore/pkg/boxlayout"
|
||||
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
||||
@@ -384,6 +385,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
|
||||
BisectInfo: git_commands.NewNullBisectInfo(),
|
||||
FilesTrie: patricia.NewTrie(),
|
||||
Authors: map[string]*models.Author{},
|
||||
MergedBranches: set.New[string](),
|
||||
},
|
||||
Modes: &types.Modes{
|
||||
Filtering: filtering.New(startArgs.FilterPath),
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
@@ -26,10 +27,11 @@ func GetBranchListDisplayStrings(
|
||||
tr *i18n.TranslationSet,
|
||||
userConfig *config.UserConfig,
|
||||
worktrees []*models.Worktree,
|
||||
mergedBranches *set.Set[string],
|
||||
) [][]string {
|
||||
return lo.Map(branches, func(branch *models.Branch, _ int) []string {
|
||||
diffed := branch.Name == diffName
|
||||
return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, tr, userConfig, worktrees)
|
||||
return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, tr, userConfig, worktrees, mergedBranches)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,6 +44,7 @@ func getBranchDisplayStrings(
|
||||
tr *i18n.TranslationSet,
|
||||
userConfig *config.UserConfig,
|
||||
worktrees []*models.Worktree,
|
||||
mergedBranches *set.Set[string],
|
||||
) []string {
|
||||
displayName := b.Name
|
||||
if b.DisplayName != "" {
|
||||
@@ -74,7 +77,15 @@ func getBranchDisplayStrings(
|
||||
}
|
||||
|
||||
if fullDescription || userConfig.Gui.ShowBranchCommitHash {
|
||||
res = append(res, utils.ShortSha(b.CommitHash))
|
||||
var commitStyle style.TextStyle
|
||||
if b.HasCommitsToPush() {
|
||||
commitStyle = getShaStyleFromStatus(models.StatusUnpushed)
|
||||
} else if mergedBranches.Includes(b.FullRefName()) {
|
||||
commitStyle = getShaStyleFromStatus(models.StatusMerged)
|
||||
} else {
|
||||
commitStyle = getShaStyleFromStatus(models.StatusPushed)
|
||||
}
|
||||
res = append(res, commitStyle.Sprint(utils.ShortSha(b.CommitHash)))
|
||||
}
|
||||
|
||||
res = append(res, coloredName)
|
||||
|
||||
@@ -413,20 +413,8 @@ func getShaColor(
|
||||
}
|
||||
|
||||
diffed := commit.Sha != "" && commit.Sha == diffName
|
||||
shaColor := theme.DefaultTextColor
|
||||
switch commit.Status {
|
||||
case models.StatusUnpushed:
|
||||
shaColor = style.FgRed
|
||||
case models.StatusPushed:
|
||||
shaColor = style.FgYellow
|
||||
case models.StatusMerged:
|
||||
shaColor = style.FgGreen
|
||||
case models.StatusRebasing:
|
||||
shaColor = style.FgBlue
|
||||
case models.StatusReflog:
|
||||
shaColor = style.FgBlue
|
||||
default:
|
||||
}
|
||||
|
||||
shaColor := getShaStyleFromStatus(commit.Status)
|
||||
|
||||
if diffed {
|
||||
shaColor = theme.DiffTerminalColor
|
||||
@@ -439,6 +427,23 @@ func getShaColor(
|
||||
return shaColor
|
||||
}
|
||||
|
||||
func getShaStyleFromStatus(status models.CommitStatus) style.TextStyle {
|
||||
switch status {
|
||||
case models.StatusUnpushed:
|
||||
return style.FgRed
|
||||
case models.StatusPushed:
|
||||
return style.FgYellow
|
||||
case models.StatusMerged:
|
||||
return style.FgGreen
|
||||
case models.StatusRebasing:
|
||||
return style.FgBlue
|
||||
case models.StatusReflog:
|
||||
return style.FgBlue
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
func actionColorMap(action todo.TodoCommand) style.TextStyle {
|
||||
switch action {
|
||||
case todo.Pick:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
@@ -223,15 +224,17 @@ type MenuItem struct {
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
CommitFiles []*models.CommitFile
|
||||
Files []*models.File
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
Commits []*models.Commit
|
||||
StashEntries []*models.StashEntry
|
||||
SubCommits []*models.Commit
|
||||
Remotes []*models.Remote
|
||||
Worktrees []*models.Worktree
|
||||
CommitFiles []*models.CommitFile
|
||||
Files []*models.File
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
// set of branch names of the form 'refs/heads/mybranch'
|
||||
MergedBranches *set.Set[string]
|
||||
Commits []*models.Commit
|
||||
StashEntries []*models.StashEntry
|
||||
SubCommits []*models.Commit
|
||||
Remotes []*models.Remote
|
||||
Worktrees []*models.Worktree
|
||||
|
||||
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
||||
// when in filtering mode we only include the ones that match the given path
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package utils
|
||||
|
||||
import "golang.org/x/exp/slices"
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// NextIndex returns the index of the element that comes after the given number
|
||||
func NextIndex(numbers []int, currentNumber int) int {
|
||||
@@ -179,3 +183,24 @@ func Shift[T any](slice []T) (T, []T) {
|
||||
slice = slice[1:]
|
||||
return value, slice
|
||||
}
|
||||
|
||||
// Map function which handles each element in its own goroutine
|
||||
func ConcurrentMap[T any, V any](items []T, fn func(T) V) []V {
|
||||
results := make([]V, len(items))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i, item := range items {
|
||||
i := i
|
||||
item := item
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
results[i] = fn(item)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -315,3 +315,13 @@ func TestMoveElement(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcurrentMap(t *testing.T) {
|
||||
in := []int{1, 2, 3}
|
||||
|
||||
out := ConcurrentMap(in, func(i int) int {
|
||||
return i * 2
|
||||
})
|
||||
|
||||
assert.EqualValues(t, []int{2, 4, 6}, out)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user