mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-25 12:24:47 +02:00
Divergence from base branch display (#3613)
- **PR Description** Add a new config option `showDivergenceFromBaseBranch`; if not "none", it indicates in the branches view for each branch if it has fallen behind its base branch, and optionally by how much. If set to "onlyArrow", it will append `↓` after the branch status; if set to "arrowAndNumber", it appends `↓17`, where the count indicates how many commits it is behind the base branch. These are colored in blue, and go after the existing yellow `↓3↑7` indication of divergence from the upstream. The option is off by default, since we are afraid that people may find this too noisy. We may reconsider this choice in the future if the response is positive. - **Please check if the PR fulfills these requirements** * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [x] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [x] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [x] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
This commit is contained in:
commit
4ac77f4575
@ -187,6 +187,10 @@ gui:
|
|||||||
# If true, show commit hashes alongside branch names in the branches view.
|
# If true, show commit hashes alongside branch names in the branches view.
|
||||||
showBranchCommitHash: false
|
showBranchCommitHash: false
|
||||||
|
|
||||||
|
# Whether to show the divergence from the base branch in the branches view.
|
||||||
|
# One of: 'none' | 'onlyArrow' | 'arrowAndNumber'
|
||||||
|
showDivergenceFromBaseBranch: none
|
||||||
|
|
||||||
# Height of the command log view
|
# Height of the command log view
|
||||||
commandLogSize: 8
|
commandLogSize: 8
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jesseduffield/generics/set"
|
"github.com/jesseduffield/generics/set"
|
||||||
"github.com/jesseduffield/go-git/v5/config"
|
"github.com/jesseduffield/go-git/v5/config"
|
||||||
@ -14,6 +15,7 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// context:
|
// context:
|
||||||
@ -63,7 +65,13 @@ func NewBranchLoader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the list of branches for the current repo
|
// Load the list of branches for the current repo
|
||||||
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
|
func (self *BranchLoader) Load(reflogCommits []*models.Commit,
|
||||||
|
mainBranches *MainBranches,
|
||||||
|
oldBranches []*models.Branch,
|
||||||
|
loadBehindCounts bool,
|
||||||
|
onWorker func(func() error),
|
||||||
|
renderFunc func(),
|
||||||
|
) ([]*models.Branch, error) {
|
||||||
branches := self.obtainBranches(self.version.IsAtLeast(2, 22, 0))
|
branches := self.obtainBranches(self.version.IsAtLeast(2, 22, 0))
|
||||||
|
|
||||||
if self.AppState.LocalBranchSortOrder == "recency" {
|
if self.AppState.LocalBranchSortOrder == "recency" {
|
||||||
@ -122,11 +130,108 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch
|
|||||||
branch.UpstreamRemote = match.Remote
|
branch.UpstreamRemote = match.Remote
|
||||||
branch.UpstreamBranch = match.Merge.Short()
|
branch.UpstreamBranch = match.Merge.Short()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the branch already existed, take over its BehindBaseBranch value
|
||||||
|
// to reduce flicker
|
||||||
|
if oldBranch, found := lo.Find(oldBranches, func(b *models.Branch) bool {
|
||||||
|
return b.Name == branch.Name
|
||||||
|
}); found {
|
||||||
|
branch.BehindBaseBranch.Store(oldBranch.BehindBaseBranch.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if loadBehindCounts && self.UserConfig.Gui.ShowDivergenceFromBaseBranch != "none" {
|
||||||
|
onWorker(func() error {
|
||||||
|
return self.GetBehindBaseBranchValuesForAllBranches(branches, mainBranches, renderFunc)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return branches, nil
|
return branches, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *BranchLoader) GetBehindBaseBranchValuesForAllBranches(
|
||||||
|
branches []*models.Branch,
|
||||||
|
mainBranches *MainBranches,
|
||||||
|
renderFunc func(),
|
||||||
|
) error {
|
||||||
|
mainBranchRefs := mainBranches.Get()
|
||||||
|
if len(mainBranchRefs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Now()
|
||||||
|
errg := errgroup.Group{}
|
||||||
|
|
||||||
|
for _, branch := range branches {
|
||||||
|
errg.Go(func() error {
|
||||||
|
baseBranch, err := self.GetBaseBranch(branch, mainBranches)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
behind := 0 // prime it in case something below fails
|
||||||
|
if baseBranch != "" {
|
||||||
|
output, err := self.cmd.New(
|
||||||
|
NewGitCmd("rev-list").
|
||||||
|
Arg("--left-right").
|
||||||
|
Arg("--count").
|
||||||
|
Arg(fmt.Sprintf("%s...%s", branch.FullRefName(), baseBranch)).
|
||||||
|
ToArgv(),
|
||||||
|
).DontLog().RunWithOutput()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// The format of the output is "<ahead>\t<behind>"
|
||||||
|
aheadBehindStr := strings.Split(strings.TrimSpace(output), "\t")
|
||||||
|
if len(aheadBehindStr) == 2 {
|
||||||
|
if value, err := strconv.Atoi(aheadBehindStr[1]); err == nil {
|
||||||
|
behind = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
branch.BehindBaseBranch.Store(int32(behind))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err := errg.Wait()
|
||||||
|
self.Log.Debugf("time to get behind base branch values for all branches: %s", time.Since(t))
|
||||||
|
renderFunc()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the base branch for the given branch (i.e. the main branch that the
|
||||||
|
// given branch was forked off of)
|
||||||
|
//
|
||||||
|
// Note that this function may return an empty string even if the returned error
|
||||||
|
// is nil, e.g. when none of the configured main branches exist. This is not
|
||||||
|
// considered an error condition, so callers need to check both the returned
|
||||||
|
// error and whether the returned base branch is empty (and possibly react
|
||||||
|
// differently in both cases).
|
||||||
|
func (self *BranchLoader) GetBaseBranch(branch *models.Branch, mainBranches *MainBranches) (string, error) {
|
||||||
|
mergeBase := mainBranches.GetMergeBase(branch.FullRefName())
|
||||||
|
if mergeBase == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := self.cmd.New(
|
||||||
|
NewGitCmd("for-each-ref").
|
||||||
|
Arg("--contains").
|
||||||
|
Arg(mergeBase).
|
||||||
|
Arg("--format=%(refname)").
|
||||||
|
Arg(mainBranches.Get()...).
|
||||||
|
ToArgv(),
|
||||||
|
).DontLog().RunWithOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
trimmedOutput := strings.TrimSpace(output)
|
||||||
|
split := strings.Split(trimmedOutput, "\n")
|
||||||
|
if len(split) == 0 || split[0] == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return split[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch {
|
func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch {
|
||||||
output, err := self.getRawBranches()
|
output, err := self.getRawBranches()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -35,11 +35,6 @@ type CommitLoader struct {
|
|||||||
readFile func(filename string) ([]byte, error)
|
readFile func(filename string) ([]byte, error)
|
||||||
walkFiles func(root string, fn filepath.WalkFunc) error
|
walkFiles func(root string, fn filepath.WalkFunc) error
|
||||||
dotGitDir string
|
dotGitDir string
|
||||||
// List of main branches that exist in the repo.
|
|
||||||
// We use these to obtain the merge base of the branch.
|
|
||||||
// When nil, we're yet to obtain the list of existing main branches.
|
|
||||||
// When an empty slice, we've obtained the list and it's empty.
|
|
||||||
mainBranches []string
|
|
||||||
*GitCommon
|
*GitCommon
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +51,6 @@ func NewCommitLoader(
|
|||||||
getRebaseMode: getRebaseMode,
|
getRebaseMode: getRebaseMode,
|
||||||
readFile: os.ReadFile,
|
readFile: os.ReadFile,
|
||||||
walkFiles: filepath.Walk,
|
walkFiles: filepath.Walk,
|
||||||
mainBranches: nil,
|
|
||||||
GitCommon: gitCommon,
|
GitCommon: gitCommon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,6 +66,7 @@ type GetCommitsOptions struct {
|
|||||||
All bool
|
All bool
|
||||||
// If non-empty, show divergence from this ref (left-right log)
|
// If non-empty, show divergence from this ref (left-right log)
|
||||||
RefToShowDivergenceFrom string
|
RefToShowDivergenceFrom string
|
||||||
|
MainBranches *MainBranches
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCommits obtains the commits of the current branch
|
// GetCommits obtains the commits of the current branch
|
||||||
@ -108,9 +103,9 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
|||||||
go utils.Safe(func() {
|
go utils.Safe(func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
ancestor = self.getMergeBase(opts.RefName)
|
ancestor = opts.MainBranches.GetMergeBase(opts.RefName)
|
||||||
if opts.RefToShowDivergenceFrom != "" {
|
if opts.RefToShowDivergenceFrom != "" {
|
||||||
remoteAncestor = self.getMergeBase(opts.RefToShowDivergenceFrom)
|
remoteAncestor = opts.MainBranches.GetMergeBase(opts.RefToShowDivergenceFrom)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -471,82 +466,6 @@ func setCommitMergedStatuses(ancestor string, commits []*models.Commit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *CommitLoader) getMergeBase(refName string) string {
|
|
||||||
if self.mainBranches == nil {
|
|
||||||
self.mainBranches = self.getExistingMainBranches()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(self.mainBranches) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// We pass all configured main branches to the merge-base call; git will
|
|
||||||
// return the base commit for the closest one.
|
|
||||||
|
|
||||||
output, err := self.cmd.New(
|
|
||||||
NewGitCmd("merge-base").Arg(refName).Arg(self.mainBranches...).
|
|
||||||
ToArgv(),
|
|
||||||
).DontLog().RunWithOutput()
|
|
||||||
if err != nil {
|
|
||||||
// If there's an error, it must be because one of the main branches that
|
|
||||||
// used to exist when we called getExistingMainBranches() was deleted
|
|
||||||
// meanwhile. To fix this for next time, throw away our cache.
|
|
||||||
self.mainBranches = nil
|
|
||||||
}
|
|
||||||
return ignoringWarnings(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
go utils.Safe(func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
// 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, 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()
|
|
||||||
|
|
||||||
existingBranches = lo.Filter(existingBranches, func(branch string, _ int) bool {
|
|
||||||
return branch != ""
|
|
||||||
})
|
|
||||||
|
|
||||||
return existingBranches
|
|
||||||
}
|
|
||||||
|
|
||||||
func ignoringWarnings(commandOutput string) string {
|
func ignoringWarnings(commandOutput string) string {
|
||||||
trimmedOutput := strings.TrimSpace(commandOutput)
|
trimmedOutput := strings.TrimSpace(commandOutput)
|
||||||
split := strings.Split(trimmedOutput, "\n")
|
split := strings.Split(trimmedOutput, "\n")
|
||||||
|
@ -307,10 +307,11 @@ func TestGetCommits(t *testing.T) {
|
|||||||
common := utils.NewDummyCommon()
|
common := utils.NewDummyCommon()
|
||||||
common.AppState = &config.AppState{}
|
common.AppState = &config.AppState{}
|
||||||
common.AppState.GitLogOrder = scenario.logOrder
|
common.AppState.GitLogOrder = scenario.logOrder
|
||||||
|
cmd := oscommands.NewDummyCmdObjBuilder(scenario.runner)
|
||||||
|
|
||||||
builder := &CommitLoader{
|
builder := &CommitLoader{
|
||||||
Common: common,
|
Common: common,
|
||||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
cmd: cmd,
|
||||||
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
|
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
|
||||||
dotGitDir: ".git",
|
dotGitDir: ".git",
|
||||||
readFile: func(filename string) ([]byte, error) {
|
readFile: func(filename string) ([]byte, error) {
|
||||||
@ -322,7 +323,9 @@ func TestGetCommits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
common.UserConfig.Git.MainBranches = scenario.mainBranches
|
common.UserConfig.Git.MainBranches = scenario.mainBranches
|
||||||
commits, err := builder.GetCommits(scenario.opts)
|
opts := scenario.opts
|
||||||
|
opts.MainBranches = NewMainBranches(scenario.mainBranches, cmd)
|
||||||
|
commits, err := builder.GetCommits(opts)
|
||||||
|
|
||||||
assert.Equal(t, scenario.expectedCommits, commits)
|
assert.Equal(t, scenario.expectedCommits, commits)
|
||||||
assert.Equal(t, scenario.expectedError, err)
|
assert.Equal(t, scenario.expectedError, err)
|
||||||
|
122
pkg/commands/git_commands/main_branches.go
Normal file
122
pkg/commands/git_commands/main_branches.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package git_commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/sasha-s/go-deadlock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MainBranches struct {
|
||||||
|
// List of main branches configured by the user. Just the bare names.
|
||||||
|
configuredMainBranches []string
|
||||||
|
// Which of these actually exist in the repository. Full ref names, and it
|
||||||
|
// could be either "refs/heads/..." or "refs/remotes/origin/..." depending
|
||||||
|
// on which one exists for a given bare name.
|
||||||
|
existingMainBranches []string
|
||||||
|
|
||||||
|
cmd oscommands.ICmdObjBuilder
|
||||||
|
mutex *deadlock.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMainBranches(
|
||||||
|
configuredMainBranches []string,
|
||||||
|
cmd oscommands.ICmdObjBuilder,
|
||||||
|
) *MainBranches {
|
||||||
|
return &MainBranches{
|
||||||
|
configuredMainBranches: configuredMainBranches,
|
||||||
|
existingMainBranches: nil,
|
||||||
|
cmd: cmd,
|
||||||
|
mutex: &deadlock.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the list of main branches that exist in the repository. This is a list of
|
||||||
|
// full ref names.
|
||||||
|
func (self *MainBranches) Get() []string {
|
||||||
|
self.mutex.Lock()
|
||||||
|
defer self.mutex.Unlock()
|
||||||
|
|
||||||
|
if self.existingMainBranches == nil {
|
||||||
|
self.existingMainBranches = self.determineMainBranches()
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.existingMainBranches
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the merge base of the given refName with the closest main branch.
|
||||||
|
func (self *MainBranches) GetMergeBase(refName string) string {
|
||||||
|
mainBranches := self.Get()
|
||||||
|
if len(mainBranches) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// We pass all existing main branches to the merge-base call; git will
|
||||||
|
// return the base commit for the closest one.
|
||||||
|
|
||||||
|
// We ignore errors from this call, since we can't distinguish whether the
|
||||||
|
// error is because one of the main branches has been deleted since the last
|
||||||
|
// call to determineMainBranches, or because the refName has no common
|
||||||
|
// history with any of the main branches. Since the former should happen
|
||||||
|
// very rarely, users must quit and restart lazygit to fix it; the latter is
|
||||||
|
// also not very common, but can totally happen and is not an error.
|
||||||
|
|
||||||
|
output, _ := self.cmd.New(
|
||||||
|
NewGitCmd("merge-base").Arg(refName).Arg(mainBranches...).
|
||||||
|
ToArgv(),
|
||||||
|
).DontLog().RunWithOutput()
|
||||||
|
return ignoringWarnings(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *MainBranches) determineMainBranches() []string {
|
||||||
|
var existingBranches []string
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
existingBranches = make([]string, len(self.configuredMainBranches))
|
||||||
|
|
||||||
|
for i, branchName := range self.configuredMainBranches {
|
||||||
|
wg.Add(1)
|
||||||
|
go utils.Safe(func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// 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, 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()
|
||||||
|
|
||||||
|
existingBranches = lo.Filter(existingBranches, func(branch string, _ int) bool {
|
||||||
|
return branch != ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return existingBranches
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
// Branch : A git branch
|
// Branch : A git branch
|
||||||
// duplicating this for now
|
// duplicating this for now
|
||||||
@ -32,6 +35,11 @@ type Branch struct {
|
|||||||
Subject string
|
Subject string
|
||||||
// commit hash
|
// commit hash
|
||||||
CommitHash string
|
CommitHash string
|
||||||
|
|
||||||
|
// How far we have fallen behind our base branch. 0 means either not
|
||||||
|
// determined yet, or up to date with base branch. (We don't need to
|
||||||
|
// distinguish the two, as we don't draw anything in both cases.)
|
||||||
|
BehindBaseBranch atomic.Int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Branch) FullRefName() string {
|
func (b *Branch) FullRefName() string {
|
||||||
|
@ -129,6 +129,9 @@ type GuiConfig struct {
|
|||||||
CommitHashLength int `yaml:"commitHashLength" jsonschema:"minimum=0"`
|
CommitHashLength int `yaml:"commitHashLength" jsonschema:"minimum=0"`
|
||||||
// If true, show commit hashes alongside branch names in the branches view.
|
// If true, show commit hashes alongside branch names in the branches view.
|
||||||
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
|
ShowBranchCommitHash bool `yaml:"showBranchCommitHash"`
|
||||||
|
// Whether to show the divergence from the base branch in the branches view.
|
||||||
|
// One of: 'none' | 'onlyArrow' | 'arrowAndNumber'
|
||||||
|
ShowDivergenceFromBaseBranch string `yaml:"showDivergenceFromBaseBranch" jsonschema:"enum=none,enum=onlyArrow,enum=arrowAndNumber"`
|
||||||
// Height of the command log view
|
// Height of the command log view
|
||||||
CommandLogSize int `yaml:"commandLogSize" jsonschema:"minimum=0"`
|
CommandLogSize int `yaml:"commandLogSize" jsonschema:"minimum=0"`
|
||||||
// Whether to split the main window when viewing file changes.
|
// Whether to split the main window when viewing file changes.
|
||||||
@ -673,27 +676,28 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
UnstagedChangesColor: []string{"red"},
|
UnstagedChangesColor: []string{"red"},
|
||||||
DefaultFgColor: []string{"default"},
|
DefaultFgColor: []string{"default"},
|
||||||
},
|
},
|
||||||
CommitLength: CommitLengthConfig{Show: true},
|
CommitLength: CommitLengthConfig{Show: true},
|
||||||
SkipNoStagedFilesWarning: false,
|
SkipNoStagedFilesWarning: false,
|
||||||
ShowListFooter: true,
|
ShowListFooter: true,
|
||||||
ShowCommandLog: true,
|
ShowCommandLog: true,
|
||||||
ShowBottomLine: true,
|
ShowBottomLine: true,
|
||||||
ShowPanelJumps: true,
|
ShowPanelJumps: true,
|
||||||
ShowFileTree: true,
|
ShowFileTree: true,
|
||||||
ShowRandomTip: true,
|
ShowRandomTip: true,
|
||||||
ShowIcons: false,
|
ShowIcons: false,
|
||||||
NerdFontsVersion: "",
|
NerdFontsVersion: "",
|
||||||
ShowFileIcons: true,
|
ShowFileIcons: true,
|
||||||
CommitHashLength: 8,
|
CommitHashLength: 8,
|
||||||
ShowBranchCommitHash: false,
|
ShowBranchCommitHash: false,
|
||||||
CommandLogSize: 8,
|
ShowDivergenceFromBaseBranch: "none",
|
||||||
SplitDiff: "auto",
|
CommandLogSize: 8,
|
||||||
SkipRewordInEditorWarning: false,
|
SplitDiff: "auto",
|
||||||
WindowSize: "normal",
|
SkipRewordInEditorWarning: false,
|
||||||
Border: "rounded",
|
WindowSize: "normal",
|
||||||
AnimateExplosion: true,
|
Border: "rounded",
|
||||||
PortraitMode: "auto",
|
AnimateExplosion: true,
|
||||||
FilterMode: "substring",
|
PortraitMode: "auto",
|
||||||
|
FilterMode: "substring",
|
||||||
Spinner: SpinnerConfig{
|
Spinner: SpinnerConfig{
|
||||||
Frames: []string{"|", "/", "-", "\\"},
|
Frames: []string{"|", "/", "-", "\\"},
|
||||||
Rate: 50,
|
Rate: 50,
|
||||||
|
@ -7,7 +7,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (config *UserConfig) Validate() error {
|
func (config *UserConfig) Validate() error {
|
||||||
if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView, []string{"dashboard", "allBranchesLog"}); err != nil {
|
if err := validateEnum("gui.statusPanelView", config.Gui.StatusPanelView,
|
||||||
|
[]string{"dashboard", "allBranchesLog"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validateEnum("gui.showDivergenceFromBaseBranch", config.Gui.ShowDivergenceFromBaseBranch,
|
||||||
|
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -130,7 +130,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
|
|||||||
if self.c.AppState.LocalBranchSortOrder == "recency" {
|
if self.c.AppState.LocalBranchSortOrder == "recency" {
|
||||||
refresh("reflog and branches", func() { self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) })
|
refresh("reflog and branches", func() { self.refreshReflogAndBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) })
|
||||||
} else {
|
} else {
|
||||||
refresh("branches", func() { self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex) })
|
refresh("branches", func() { self.refreshBranches(includeWorktreesWithBranches, options.KeepBranchSelectionIndex, true) })
|
||||||
refresh("reflog", func() { _ = self.refreshReflogCommits() })
|
refresh("reflog", func() { _ = self.refreshReflogCommits() })
|
||||||
}
|
}
|
||||||
} else if scopeSet.Includes(types.REBASE_COMMITS) {
|
} else if scopeSet.Includes(types.REBASE_COMMITS) {
|
||||||
@ -256,7 +256,7 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
|
|||||||
case types.INITIAL:
|
case types.INITIAL:
|
||||||
self.c.OnWorker(func(_ gocui.Task) error {
|
self.c.OnWorker(func(_ gocui.Task) error {
|
||||||
_ = self.refreshReflogCommits()
|
_ = self.refreshReflogCommits()
|
||||||
self.refreshBranches(false, true)
|
self.refreshBranches(false, true, true)
|
||||||
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)
|
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -267,9 +267,11 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *RefreshHelper) refreshReflogAndBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) {
|
func (self *RefreshHelper) refreshReflogAndBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) {
|
||||||
|
loadBehindCounts := self.c.State().GetRepoState().GetStartupStage() == types.COMPLETE
|
||||||
|
|
||||||
self.refreshReflogCommitsConsideringStartup()
|
self.refreshReflogCommitsConsideringStartup()
|
||||||
|
|
||||||
self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex)
|
self.refreshBranches(refreshWorktrees, keepBranchSelectionIndex, loadBehindCounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *RefreshHelper) refreshCommitsAndCommitFiles() {
|
func (self *RefreshHelper) refreshCommitsAndCommitFiles() {
|
||||||
@ -331,6 +333,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error {
|
|||||||
RefName: self.refForLog(),
|
RefName: self.refForLog(),
|
||||||
RefForPushedStatus: checkedOutBranchName,
|
RefForPushedStatus: checkedOutBranchName,
|
||||||
All: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(),
|
All: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(),
|
||||||
|
MainBranches: self.c.Model().MainBranches,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -357,6 +360,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
|
|||||||
RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(),
|
RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(),
|
||||||
RefToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(),
|
RefToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(),
|
||||||
RefForPushedStatus: self.c.Contexts().SubCommits.GetRef().FullRefName(),
|
RefForPushedStatus: self.c.Contexts().SubCommits.GetRef().FullRefName(),
|
||||||
|
MainBranches: self.c.Model().MainBranches,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -436,7 +440,7 @@ func (self *RefreshHelper) refreshStateSubmoduleConfigs() error {
|
|||||||
|
|
||||||
// self.refreshStatus is called at the end of this because that's when we can
|
// self.refreshStatus is called at the end of this because that's when we can
|
||||||
// be sure there is a State.Model.Branches array to pick the current branch from
|
// be sure there is a State.Model.Branches array to pick the current branch from
|
||||||
func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool) {
|
func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSelectionIndex bool, loadBehindCounts bool) {
|
||||||
self.c.Mutexes().RefreshingBranchesMutex.Lock()
|
self.c.Mutexes().RefreshingBranchesMutex.Lock()
|
||||||
defer self.c.Mutexes().RefreshingBranchesMutex.Unlock()
|
defer self.c.Mutexes().RefreshingBranchesMutex.Unlock()
|
||||||
|
|
||||||
@ -455,7 +459,25 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
branches, err := self.c.Git().Loaders.BranchLoader.Load(reflogCommits)
|
branches, err := self.c.Git().Loaders.BranchLoader.Load(
|
||||||
|
reflogCommits,
|
||||||
|
self.c.Model().MainBranches,
|
||||||
|
self.c.Model().Branches,
|
||||||
|
loadBehindCounts,
|
||||||
|
func(f func() error) {
|
||||||
|
self.c.OnWorker(func(_ gocui.Task) error {
|
||||||
|
return f()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
func() {
|
||||||
|
self.c.OnUIThread(func() error {
|
||||||
|
if err := self.c.Contexts().Branches.HandleRender(); err != nil {
|
||||||
|
self.c.Log.Error(err)
|
||||||
|
}
|
||||||
|
self.refreshStatus()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
self.c.Log.Error(err)
|
self.c.Log.Error(err)
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error {
|
|||||||
RefName: opts.Ref.FullRefName(),
|
RefName: opts.Ref.FullRefName(),
|
||||||
RefForPushedStatus: opts.Ref.FullRefName(),
|
RefForPushedStatus: opts.Ref.FullRefName(),
|
||||||
RefToShowDivergenceFrom: opts.RefToShowDivergenceFrom,
|
RefToShowDivergenceFrom: opts.RefToShowDivergenceFrom,
|
||||||
|
MainBranches: self.c.Model().MainBranches,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -116,7 +117,7 @@ func (self *StatusController) onClick(opts gocui.ViewMouseBindingOpts) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
upstreamStatus := presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr, time.Now(), self.c.UserConfig)
|
upstreamStatus := utils.Decolorise(presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr, time.Now(), self.c.UserConfig))
|
||||||
repoName := self.c.Git().RepoPaths.RepoName()
|
repoName := self.c.Git().RepoPaths.RepoName()
|
||||||
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
||||||
switch workingTreeState {
|
switch workingTreeState {
|
||||||
|
@ -379,6 +379,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
|
|||||||
BisectInfo: git_commands.NewNullBisectInfo(),
|
BisectInfo: git_commands.NewNullBisectInfo(),
|
||||||
FilesTrie: patricia.NewTrie(),
|
FilesTrie: patricia.NewTrie(),
|
||||||
Authors: map[string]*models.Author{},
|
Authors: map[string]*models.Author{},
|
||||||
|
MainBranches: git_commands.NewMainBranches(gui.UserConfig.Git.MainBranches, gui.os.Cmd),
|
||||||
},
|
},
|
||||||
Modes: &types.Modes{
|
Modes: &types.Modes{
|
||||||
Filtering: filtering.New(startArgs.FilterPath, ""),
|
Filtering: filtering.New(startArgs.FilterPath, ""),
|
||||||
|
@ -56,7 +56,7 @@ func getBranchDisplayStrings(
|
|||||||
// Recency is always three characters, plus one for the space
|
// Recency is always three characters, plus one for the space
|
||||||
availableWidth := viewWidth - 4
|
availableWidth := viewWidth - 4
|
||||||
if len(branchStatus) > 0 {
|
if len(branchStatus) > 0 {
|
||||||
availableWidth -= runewidth.StringWidth(branchStatus) + 1
|
availableWidth -= runewidth.StringWidth(utils.Decolorise(branchStatus)) + 1
|
||||||
}
|
}
|
||||||
if icons.IsIconEnabled() {
|
if icons.IsIconEnabled() {
|
||||||
availableWidth -= 2 // one for the icon, one for the space
|
availableWidth -= 2 // one for the icon, one for the space
|
||||||
@ -89,8 +89,7 @@ func getBranchDisplayStrings(
|
|||||||
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
|
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
|
||||||
}
|
}
|
||||||
if len(branchStatus) > 0 {
|
if len(branchStatus) > 0 {
|
||||||
coloredStatus := branchStatusColor(b, itemOperation).Sprint(branchStatus)
|
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
|
||||||
coloredName = fmt.Sprintf("%s %s", coloredName, coloredStatus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recencyColor := style.FgCyan
|
recencyColor := style.FgCyan
|
||||||
@ -144,30 +143,6 @@ func GetBranchTextStyle(name string) style.TextStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func branchStatusColor(branch *models.Branch, itemOperation types.ItemOperation) style.TextStyle {
|
|
||||||
colour := style.FgYellow
|
|
||||||
if itemOperation != types.ItemOperationNone {
|
|
||||||
colour = style.FgCyan
|
|
||||||
} else if branch.UpstreamGone {
|
|
||||||
colour = style.FgRed
|
|
||||||
} else if branch.MatchesUpstream() {
|
|
||||||
colour = style.FgGreen
|
|
||||||
} else if branch.RemoteBranchNotStoredLocally() {
|
|
||||||
colour = style.FgMagenta
|
|
||||||
}
|
|
||||||
|
|
||||||
return colour
|
|
||||||
}
|
|
||||||
|
|
||||||
func ColoredBranchStatus(
|
|
||||||
branch *models.Branch,
|
|
||||||
itemOperation types.ItemOperation,
|
|
||||||
tr *i18n.TranslationSet,
|
|
||||||
userConfig *config.UserConfig,
|
|
||||||
) string {
|
|
||||||
return branchStatusColor(branch, itemOperation).Sprint(BranchStatus(branch, itemOperation, tr, time.Now(), userConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
func BranchStatus(
|
func BranchStatus(
|
||||||
branch *models.Branch,
|
branch *models.Branch,
|
||||||
itemOperation types.ItemOperation,
|
itemOperation types.ItemOperation,
|
||||||
@ -177,30 +152,38 @@ func BranchStatus(
|
|||||||
) string {
|
) string {
|
||||||
itemOperationStr := ItemOperationToString(itemOperation, tr)
|
itemOperationStr := ItemOperationToString(itemOperation, tr)
|
||||||
if itemOperationStr != "" {
|
if itemOperationStr != "" {
|
||||||
return itemOperationStr + " " + utils.Loader(now, userConfig.Gui.Spinner)
|
return style.FgCyan.Sprintf("%s %s", itemOperationStr, utils.Loader(now, userConfig.Gui.Spinner))
|
||||||
}
|
|
||||||
|
|
||||||
if !branch.IsTrackingRemote() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if branch.UpstreamGone {
|
|
||||||
return tr.UpstreamGone
|
|
||||||
}
|
|
||||||
|
|
||||||
if branch.MatchesUpstream() {
|
|
||||||
return "✓"
|
|
||||||
}
|
|
||||||
if branch.RemoteBranchNotStoredLocally() {
|
|
||||||
return "?"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result := ""
|
result := ""
|
||||||
if branch.IsAheadForPull() {
|
if branch.IsTrackingRemote() {
|
||||||
result = fmt.Sprintf("↑%s", branch.AheadForPull)
|
if branch.UpstreamGone {
|
||||||
|
result = style.FgRed.Sprint(tr.UpstreamGone)
|
||||||
|
} else if branch.MatchesUpstream() {
|
||||||
|
result = style.FgGreen.Sprint("✓")
|
||||||
|
} else if branch.RemoteBranchNotStoredLocally() {
|
||||||
|
result = style.FgMagenta.Sprint("?")
|
||||||
|
} else if branch.IsBehindForPull() && branch.IsAheadForPull() {
|
||||||
|
result = style.FgYellow.Sprintf("↓%s↑%s", branch.BehindForPull, branch.AheadForPull)
|
||||||
|
} else if branch.IsBehindForPull() {
|
||||||
|
result = style.FgYellow.Sprintf("↓%s", branch.BehindForPull)
|
||||||
|
} else if branch.IsAheadForPull() {
|
||||||
|
result = style.FgYellow.Sprintf("↑%s", branch.AheadForPull)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if branch.IsBehindForPull() {
|
|
||||||
result = fmt.Sprintf("%s↓%s", result, branch.BehindForPull)
|
if userConfig.Gui.ShowDivergenceFromBaseBranch != "none" {
|
||||||
|
behind := branch.BehindBaseBranch.Load()
|
||||||
|
if behind != 0 {
|
||||||
|
if result != "" {
|
||||||
|
result += " "
|
||||||
|
}
|
||||||
|
if userConfig.Gui.ShowDivergenceFromBaseBranch == "arrowAndNumber" {
|
||||||
|
result += style.FgCyan.Sprintf("↓%d", behind)
|
||||||
|
} else {
|
||||||
|
result += style.FgCyan.Sprintf("↓")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -2,6 +2,7 @@ package presentation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -15,6 +16,11 @@ import (
|
|||||||
"github.com/xo/terminfo"
|
"github.com/xo/terminfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func makeAtomic(v int32) (result atomic.Int32) {
|
||||||
|
result.Store(v)
|
||||||
|
return //nolint: nakedret
|
||||||
|
}
|
||||||
|
|
||||||
func Test_getBranchDisplayStrings(t *testing.T) {
|
func Test_getBranchDisplayStrings(t *testing.T) {
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
branch *models.Branch
|
branch *models.Branch
|
||||||
@ -23,6 +29,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth int
|
viewWidth int
|
||||||
useIcons bool
|
useIcons bool
|
||||||
checkedOutByWorktree bool
|
checkedOutByWorktree bool
|
||||||
|
showDivergenceCfg string
|
||||||
expected []string
|
expected []string
|
||||||
}{
|
}{
|
||||||
// First some tests for when the view is wide enough so that everything fits:
|
// First some tests for when the view is wide enough so that everything fits:
|
||||||
@ -33,6 +40,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 100,
|
viewWidth: 100,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "branch_name"},
|
expected: []string{"1m", "branch_name"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -42,6 +50,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 100,
|
viewWidth: 100,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: true,
|
checkedOutByWorktree: true,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "branch_name (worktree)"},
|
expected: []string{"1m", "branch_name (worktree)"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -51,6 +60,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 100,
|
viewWidth: 100,
|
||||||
useIcons: true,
|
useIcons: true,
|
||||||
checkedOutByWorktree: true,
|
checkedOutByWorktree: true,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "", "branch_name "},
|
expected: []string{"1m", "", "branch_name "},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -66,6 +76,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 100,
|
viewWidth: 100,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "branch_name ✓"},
|
expected: []string{"1m", "branch_name ✓"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -81,7 +92,56 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 100,
|
viewWidth: 100,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: true,
|
checkedOutByWorktree: true,
|
||||||
expected: []string{"1m", "branch_name (worktree) ↑3↓5"},
|
showDivergenceCfg: "none",
|
||||||
|
expected: []string{"1m", "branch_name (worktree) ↓5↑3"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
branch: &models.Branch{
|
||||||
|
Name: "branch_name",
|
||||||
|
Recency: "1m",
|
||||||
|
BehindBaseBranch: makeAtomic(2),
|
||||||
|
},
|
||||||
|
itemOperation: types.ItemOperationNone,
|
||||||
|
fullDescription: false,
|
||||||
|
viewWidth: 100,
|
||||||
|
useIcons: false,
|
||||||
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "onlyArrow",
|
||||||
|
expected: []string{"1m", "branch_name ↓"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
branch: &models.Branch{
|
||||||
|
Name: "branch_name",
|
||||||
|
Recency: "1m",
|
||||||
|
UpstreamRemote: "origin",
|
||||||
|
AheadForPull: "0",
|
||||||
|
BehindForPull: "0",
|
||||||
|
BehindBaseBranch: makeAtomic(2),
|
||||||
|
},
|
||||||
|
itemOperation: types.ItemOperationNone,
|
||||||
|
fullDescription: false,
|
||||||
|
viewWidth: 100,
|
||||||
|
useIcons: false,
|
||||||
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "arrowAndNumber",
|
||||||
|
expected: []string{"1m", "branch_name ✓ ↓2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
branch: &models.Branch{
|
||||||
|
Name: "branch_name",
|
||||||
|
Recency: "1m",
|
||||||
|
UpstreamRemote: "origin",
|
||||||
|
AheadForPull: "3",
|
||||||
|
BehindForPull: "5",
|
||||||
|
BehindBaseBranch: makeAtomic(2),
|
||||||
|
},
|
||||||
|
itemOperation: types.ItemOperationNone,
|
||||||
|
fullDescription: false,
|
||||||
|
viewWidth: 100,
|
||||||
|
useIcons: false,
|
||||||
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "arrowAndNumber",
|
||||||
|
expected: []string{"1m", "branch_name ↓5↑3 ↓2"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||||
@ -90,6 +150,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 100,
|
viewWidth: 100,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "branch_name Pushing |"},
|
expected: []string{"1m", "branch_name Pushing |"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -108,6 +169,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 100,
|
viewWidth: 100,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "12345678", "branch_name ✓", "origin branch_name", "commit title"},
|
expected: []string{"1m", "12345678", "branch_name ✓", "origin branch_name", "commit title"},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -119,6 +181,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 14,
|
viewWidth: 14,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "branch_na…"},
|
expected: []string{"1m", "branch_na…"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -128,6 +191,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 14,
|
viewWidth: 14,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: true,
|
checkedOutByWorktree: true,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "bra… (worktree)"},
|
expected: []string{"1m", "bra… (worktree)"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -137,6 +201,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 14,
|
viewWidth: 14,
|
||||||
useIcons: true,
|
useIcons: true,
|
||||||
checkedOutByWorktree: true,
|
checkedOutByWorktree: true,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "", "branc… "},
|
expected: []string{"1m", "", "branc… "},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -152,6 +217,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 14,
|
viewWidth: 14,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "branch_… ✓"},
|
expected: []string{"1m", "branch_… ✓"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -167,7 +233,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 30,
|
viewWidth: 30,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: true,
|
checkedOutByWorktree: true,
|
||||||
expected: []string{"1m", "branch_na… (worktree) ↑3↓5"},
|
showDivergenceCfg: "none",
|
||||||
|
expected: []string{"1m", "branch_na… (worktree) ↓5↑3"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
branch: &models.Branch{Name: "branch_name", Recency: "1m"},
|
||||||
@ -176,6 +243,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 20,
|
viewWidth: 20,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "branc… Pushing |"},
|
expected: []string{"1m", "branc… Pushing |"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -185,6 +253,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: -1,
|
viewWidth: -1,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "abc Pushing |"},
|
expected: []string{"1m", "abc Pushing |"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -194,6 +263,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: -1,
|
viewWidth: -1,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "ab Pushing |"},
|
expected: []string{"1m", "ab Pushing |"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -203,6 +273,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: -1,
|
viewWidth: -1,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "a Pushing |"},
|
expected: []string{"1m", "a Pushing |"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -221,6 +292,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
viewWidth: 20,
|
viewWidth: 20,
|
||||||
useIcons: false,
|
useIcons: false,
|
||||||
checkedOutByWorktree: false,
|
checkedOutByWorktree: false,
|
||||||
|
showDivergenceCfg: "none",
|
||||||
expected: []string{"1m", "12345678", "bran… ✓", "origin branch_name", "commit title"},
|
expected: []string{"1m", "12345678", "bran… ✓", "origin branch_name", "commit title"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -232,6 +304,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
|||||||
|
|
||||||
for i, s := range scenarios {
|
for i, s := range scenarios {
|
||||||
icons.SetNerdFontsVersion(lo.Ternary(s.useIcons, "3", ""))
|
icons.SetNerdFontsVersion(lo.Ternary(s.useIcons, "3", ""))
|
||||||
|
c.UserConfig.Gui.ShowDivergenceFromBaseBranch = s.showDivergenceCfg
|
||||||
|
|
||||||
worktrees := []*models.Worktree{}
|
worktrees := []*models.Worktree{}
|
||||||
if s.checkedOutByWorktree {
|
if s.checkedOutByWorktree {
|
||||||
|
@ -2,6 +2,7 @@ package presentation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||||
@ -24,7 +25,10 @@ func FormatStatus(
|
|||||||
status := ""
|
status := ""
|
||||||
|
|
||||||
if currentBranch.IsRealBranch() {
|
if currentBranch.IsRealBranch() {
|
||||||
status += ColoredBranchStatus(currentBranch, itemOperation, tr, userConfig) + " "
|
status += BranchStatus(currentBranch, itemOperation, tr, time.Now(), userConfig)
|
||||||
|
if status != "" {
|
||||||
|
status += " "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if workingTreeState != enums.REBASE_MODE_NONE {
|
if workingTreeState != enums.REBASE_MODE_NONE {
|
||||||
@ -40,7 +44,7 @@ func FormatStatus(
|
|||||||
}
|
}
|
||||||
repoName = fmt.Sprintf("%s(%s%s)", repoName, icon, style.FgCyan.Sprint(linkedWorktreeName))
|
repoName = fmt.Sprintf("%s(%s%s)", repoName, icon, style.FgCyan.Sprint(linkedWorktreeName))
|
||||||
}
|
}
|
||||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
status += fmt.Sprintf("%s → %s", repoName, name)
|
||||||
|
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
@ -281,6 +281,8 @@ type Model struct {
|
|||||||
// we're on a detached head because we're rebasing or bisecting.
|
// we're on a detached head because we're rebasing or bisecting.
|
||||||
CheckedOutBranch string
|
CheckedOutBranch string
|
||||||
|
|
||||||
|
MainBranches *git_commands.MainBranches
|
||||||
|
|
||||||
// for displaying suggestions while typing in a file name
|
// for displaying suggestions while typing in a file name
|
||||||
FilesTrie *patricia.Trie
|
FilesTrie *patricia.Trie
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTe
|
|||||||
Confirm()
|
Confirm()
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → mybranch"))
|
t.Views().Status().Content(Equals("✓ repo → mybranch"))
|
||||||
|
|
||||||
deleteBranch()
|
deleteBranch()
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTe
|
|||||||
Content(Contains("incorrect username/password")).
|
Content(Contains("incorrect username/password")).
|
||||||
Confirm()
|
Confirm()
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → mybranch"))
|
t.Views().Status().Content(Equals("✓ repo → mybranch"))
|
||||||
|
|
||||||
// try again with correct password
|
// try again with correct password
|
||||||
deleteBranch()
|
deleteBranch()
|
||||||
@ -81,7 +81,7 @@ var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTe
|
|||||||
Type("password").
|
Type("password").
|
||||||
Confirm()
|
Confirm()
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("repo → mybranch").DoesNotContain("✓"))
|
t.Views().Status().Content(Equals("(upstream gone) repo → mybranch"))
|
||||||
t.Views().Branches().TopLines(Contains("mybranch (upstream gone)"))
|
t.Views().Branches().TopLines(Contains("mybranch (upstream gone)"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ShowDivergenceFromBaseBranch = NewIntegrationTest(NewIntegrationTestArgs{
|
||||||
|
Description: "Show divergence from base branch in the status panel",
|
||||||
|
ExtraCmdArgs: []string{},
|
||||||
|
Skip: false,
|
||||||
|
SetupConfig: func(config *config.AppConfig) {
|
||||||
|
config.UserConfig.Gui.ShowDivergenceFromBaseBranch = "arrowAndNumber"
|
||||||
|
},
|
||||||
|
SetupRepo: func(shell *Shell) {
|
||||||
|
shell.CreateNCommits(2)
|
||||||
|
shell.CloneIntoRemote("origin")
|
||||||
|
shell.NewBranch("feature")
|
||||||
|
shell.HardReset("HEAD^")
|
||||||
|
},
|
||||||
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
|
t.GlobalPress(keys.Universal.NextBlock)
|
||||||
|
|
||||||
|
t.Views().Status().
|
||||||
|
Content(Equals("↓1 repo → feature"))
|
||||||
|
},
|
||||||
|
})
|
@ -26,7 +26,7 @@ var ForcePush = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓1 repo → master"))
|
t.Views().Status().Content(Equals("↓1 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().IsFocused().Press(keys.Universal.Push)
|
t.Views().Files().IsFocused().Press(keys.Universal.Push)
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ var ForcePush = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
|
|
||||||
t.Views().Remotes().Focus().
|
t.Views().Remotes().Focus().
|
||||||
Lines(Contains("origin")).
|
Lines(Contains("origin")).
|
||||||
|
@ -22,7 +22,7 @@ var ForcePushMultipleMatching = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓1 repo → master"))
|
t.Views().Status().Content(Equals("↓1 repo → master"))
|
||||||
|
|
||||||
t.Views().Branches().
|
t.Views().Branches().
|
||||||
Lines(
|
Lines(
|
||||||
@ -42,7 +42,7 @@ var ForcePushMultipleMatching = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
|
|
||||||
t.Views().Branches().
|
t.Views().Branches().
|
||||||
Lines(
|
Lines(
|
||||||
|
@ -21,7 +21,7 @@ var ForcePushMultipleUpstream = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓1 repo → master"))
|
t.Views().Status().Content(Equals("↓1 repo → master"))
|
||||||
|
|
||||||
t.Views().Branches().
|
t.Views().Branches().
|
||||||
Lines(
|
Lines(
|
||||||
@ -41,7 +41,7 @@ var ForcePushMultipleUpstream = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
|
|
||||||
t.Views().Branches().
|
t.Views().Branches().
|
||||||
Lines(
|
Lines(
|
||||||
|
@ -26,7 +26,7 @@ var Pull = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓1 repo → master"))
|
t.Views().Status().Content(Equals("↓1 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().IsFocused().Press(keys.Universal.Pull)
|
t.Views().Files().IsFocused().Press(keys.Universal.Pull)
|
||||||
|
|
||||||
@ -36,6 +36,6 @@ var Pull = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -25,7 +25,7 @@ var PullAndSetUpstream = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("repo → master"))
|
t.Views().Status().Content(Equals("repo → master"))
|
||||||
|
|
||||||
t.Views().Files().IsFocused().Press(keys.Universal.Pull)
|
t.Views().Files().IsFocused().Press(keys.Universal.Pull)
|
||||||
|
|
||||||
@ -40,6 +40,6 @@ var PullAndSetUpstream = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -33,13 +33,13 @@ var PullMerge = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓2 repo → master"))
|
t.Views().Status().Content(Equals("↓2↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
Press(keys.Universal.Pull)
|
Press(keys.Universal.Pull)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↑2 repo → master"))
|
t.Views().Status().Content(Equals("↑2 repo → master"))
|
||||||
|
|
||||||
t.Views().Commits().
|
t.Views().Commits().
|
||||||
Lines(
|
Lines(
|
||||||
|
@ -34,7 +34,7 @@ var PullMergeConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓2 repo → master"))
|
t.Views().Status().Content(Equals("↓2↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
@ -62,7 +62,7 @@ var PullMergeConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
|
|
||||||
t.Common().ContinueOnConflictsResolved()
|
t.Common().ContinueOnConflictsResolved()
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↑2 repo → master"))
|
t.Views().Status().Content(Equals("↑2 repo → master"))
|
||||||
|
|
||||||
t.Views().Commits().
|
t.Views().Commits().
|
||||||
Focus().
|
Focus().
|
||||||
|
@ -35,13 +35,13 @@ var PullRebase = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓2 repo → master"))
|
t.Views().Status().Content(Equals("↓2↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
Press(keys.Universal.Pull)
|
Press(keys.Universal.Pull)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↑1 repo → master"))
|
t.Views().Status().Content(Equals("↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Commits().
|
t.Views().Commits().
|
||||||
Lines(
|
Lines(
|
||||||
|
@ -34,7 +34,7 @@ var PullRebaseConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓2 repo → master"))
|
t.Views().Status().Content(Equals("↓2↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
@ -63,7 +63,7 @@ var PullRebaseConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
|
|
||||||
t.Common().ContinueOnConflictsResolved()
|
t.Common().ContinueOnConflictsResolved()
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↑1 repo → master"))
|
t.Views().Status().Content(Equals("↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Commits().
|
t.Views().Commits().
|
||||||
Focus().
|
Focus().
|
||||||
|
@ -38,7 +38,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓2 repo → master"))
|
t.Views().Status().Content(Equals("↓2↑2 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
@ -76,7 +76,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
|
|
||||||
t.Common().ContinueOnConflictsResolved()
|
t.Common().ContinueOnConflictsResolved()
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↑2 repo → master"))
|
t.Views().Status().Content(Equals("↑2 repo → master"))
|
||||||
|
|
||||||
t.Views().Commits().
|
t.Views().Commits().
|
||||||
Focus().
|
Focus().
|
||||||
|
@ -38,7 +38,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg
|
|||||||
Contains("one"),
|
Contains("one"),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↓2 repo → master"))
|
t.Views().Status().Content(Equals("↓2↑2 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
@ -85,7 +85,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg
|
|||||||
|
|
||||||
t.Common().ContinueOnConflictsResolved()
|
t.Common().ContinueOnConflictsResolved()
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↑1 repo → master"))
|
t.Views().Status().Content(Equals("↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Commits().
|
t.Views().Commits().
|
||||||
Focus().
|
Focus().
|
||||||
|
@ -21,7 +21,7 @@ var Push = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
shell.EmptyCommit("two")
|
shell.EmptyCommit("two")
|
||||||
},
|
},
|
||||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
t.Views().Status().Content(Contains("↑1 repo → master"))
|
t.Views().Status().Content(Equals("↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
|
@ -22,7 +22,7 @@ var PushAndAutoSetUpstream = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
},
|
},
|
||||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
// assert no mention of upstream/downstream changes
|
// assert no mention of upstream/downstream changes
|
||||||
t.Views().Status().Content(MatchesRegexp(`^\s+repo → master`))
|
t.Views().Status().Content(Equals("repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
|
@ -19,7 +19,7 @@ var PushAndSetUpstream = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
},
|
},
|
||||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
// assert no mention of upstream/downstream changes
|
// assert no mention of upstream/downstream changes
|
||||||
t.Views().Status().Content(MatchesRegexp(`^\s+repo → master`))
|
t.Views().Status().Content(Equals("repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
|
@ -24,13 +24,13 @@ var PushFollowTags = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
shell.SetConfig("push.followTags", "true")
|
shell.SetConfig("push.followTags", "true")
|
||||||
},
|
},
|
||||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
t.Views().Status().Content(Contains("↑1 repo → master"))
|
t.Views().Status().Content(Equals("↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
Press(keys.Universal.Push)
|
Press(keys.Universal.Push)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
|
|
||||||
t.Views().Remotes().
|
t.Views().Remotes().
|
||||||
Focus().
|
Focus().
|
||||||
|
@ -22,13 +22,13 @@ var PushNoFollowTags = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
shell.CreateAnnotatedTag("mytag", "message", "HEAD")
|
shell.CreateAnnotatedTag("mytag", "message", "HEAD")
|
||||||
},
|
},
|
||||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
Press(keys.Universal.Push)
|
Press(keys.Universal.Push)
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
|
|
||||||
t.Views().Remotes().
|
t.Views().Remotes().
|
||||||
Focus().
|
Focus().
|
||||||
|
@ -26,7 +26,7 @@ var PushWithCredentialPrompt = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
shell.CopyHelpFile("pre-push", ".git/hooks/pre-push")
|
shell.CopyHelpFile("pre-push", ".git/hooks/pre-push")
|
||||||
},
|
},
|
||||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||||
t.Views().Status().Content(Contains("↑1 repo → master"))
|
t.Views().Status().Content(Equals("↑1 repo → master"))
|
||||||
|
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
@ -50,7 +50,7 @@ var PushWithCredentialPrompt = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Content(Contains("incorrect username/password")).
|
Content(Contains("incorrect username/password")).
|
||||||
Confirm()
|
Confirm()
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("↑1 repo → master"))
|
t.Views().Status().Content(Equals("↑1 repo → master"))
|
||||||
|
|
||||||
// try again with correct password
|
// try again with correct password
|
||||||
t.Views().Files().
|
t.Views().Files().
|
||||||
@ -67,7 +67,7 @@ var PushWithCredentialPrompt = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
Type("password").
|
Type("password").
|
||||||
Confirm()
|
Confirm()
|
||||||
|
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
|
|
||||||
assertSuccessfullyPushed(t)
|
assertSuccessfullyPushed(t)
|
||||||
},
|
},
|
||||||
|
@ -24,7 +24,7 @@ func createTwoBranchesReadyToForcePush(shell *Shell) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func assertSuccessfullyPushed(t *TestDriver) {
|
func assertSuccessfullyPushed(t *TestDriver) {
|
||||||
t.Views().Status().Content(Contains("✓ repo → master"))
|
t.Views().Status().Content(Equals("✓ repo → master"))
|
||||||
|
|
||||||
t.Views().Remotes().
|
t.Views().Remotes().
|
||||||
Focus().
|
Focus().
|
||||||
|
@ -266,6 +266,7 @@ var tests = []*components.IntegrationTest{
|
|||||||
status.ClickRepoNameToOpenReposMenu,
|
status.ClickRepoNameToOpenReposMenu,
|
||||||
status.ClickToFocus,
|
status.ClickToFocus,
|
||||||
status.ClickWorkingTreeStateToOpenRebaseOptionsMenu,
|
status.ClickWorkingTreeStateToOpenRebaseOptionsMenu,
|
||||||
|
status.ShowDivergenceFromBaseBranch,
|
||||||
submodule.Add,
|
submodule.Add,
|
||||||
submodule.Enter,
|
submodule.Enter,
|
||||||
submodule.EnterNested,
|
submodule.EnterNested,
|
||||||
|
@ -331,6 +331,16 @@
|
|||||||
"description": "If true, show commit hashes alongside branch names in the branches view.",
|
"description": "If true, show commit hashes alongside branch names in the branches view.",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
|
"showDivergenceFromBaseBranch": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none",
|
||||||
|
"onlyArrow",
|
||||||
|
"arrowAndNumber"
|
||||||
|
],
|
||||||
|
"description": "Whether to show the divergence from the base branch in the branches view.\nOne of: 'none' | 'onlyArrow' | 'arrowAndNumber'",
|
||||||
|
"default": "none"
|
||||||
|
},
|
||||||
"commandLogSize": {
|
"commandLogSize": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user