From dde30fa104347ab9c01f82b7886864b473e6f51c Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Thu, 24 Mar 2022 17:49:25 +1100 Subject: [PATCH] add gone branches status --- pkg/commands/loaders/branches.go | 78 +++++++++++++++------------ pkg/commands/loaders/branches_test.go | 52 ++++++++++++++++++ pkg/commands/models/branch.go | 11 ++-- pkg/gui/list_context_config.go | 2 +- pkg/gui/presentation/branches.go | 24 +++++---- pkg/gui/refresh.go | 2 +- pkg/gui/status_panel.go | 2 +- pkg/i18n/english.go | 2 + 8 files changed, 121 insertions(+), 52 deletions(-) create mode 100644 pkg/commands/loaders/branches_test.go diff --git a/pkg/commands/loaders/branches.go b/pkg/commands/loaders/branches.go index c54e65ee9..1f78908a8 100644 --- a/pkg/commands/loaders/branches.go +++ b/pkg/commands/loaders/branches.go @@ -106,6 +106,49 @@ outer: return branches, nil } +// Obtain branch information from parsed line output of getRawBranches() +// split contains the '|' separated tokens in the line of output +func obtainBranch(split []string) *models.Branch { + name := strings.TrimPrefix(split[1], "heads/") + branch := &models.Branch{ + Name: name, + Pullables: "?", + Pushables: "?", + Head: split[0] == "*", + } + + upstreamName := split[2] + if upstreamName == "" { + // if we're here then it means we do not have a local version of the remote. + // The branch might still be tracking a remote though, we just don't know + // how many commits ahead/behind it is + return branch + } + + track := split[3] + if track == "[gone]" { + branch.UpstreamGone = true + } else { + re := regexp.MustCompile(`ahead (\d+)`) + match := re.FindStringSubmatch(track) + if len(match) > 1 { + branch.Pushables = match[1] + } else { + branch.Pushables = "0" + } + + re = regexp.MustCompile(`behind (\d+)`) + match = re.FindStringSubmatch(track) + if len(match) > 1 { + branch.Pullables = match[1] + } else { + branch.Pullables = "0" + } + } + + return branch +} + func (self *BranchLoader) obtainBranches() []*models.Branch { output, err := self.getRawBranches() if err != nil { @@ -128,40 +171,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch { continue } - name := strings.TrimPrefix(split[1], "heads/") - branch := &models.Branch{ - Name: name, - Pullables: "?", - Pushables: "?", - Head: split[0] == "*", - } - - upstreamName := split[2] - if upstreamName == "" { - // if we're here then it means we do not have a local version of the remote. - // The branch might still be tracking a remote though, we just don't know - // how many commits ahead/behind it is - branches = append(branches, branch) - continue - } - - track := split[3] - re := regexp.MustCompile(`ahead (\d+)`) - match := re.FindStringSubmatch(track) - if len(match) > 1 { - branch.Pushables = match[1] - } else { - branch.Pushables = "0" - } - - re = regexp.MustCompile(`behind (\d+)`) - match = re.FindStringSubmatch(track) - if len(match) > 1 { - branch.Pullables = match[1] - } else { - branch.Pullables = "0" - } - + branch := obtainBranch(split) branches = append(branches, branch) } diff --git a/pkg/commands/loaders/branches_test.go b/pkg/commands/loaders/branches_test.go new file mode 100644 index 000000000..70f02dcf7 --- /dev/null +++ b/pkg/commands/loaders/branches_test.go @@ -0,0 +1,52 @@ +package loaders + +// "*|feat/detect-purge|origin/feat/detect-purge|[ahead 1]" +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/stretchr/testify/assert" +) + +func TestObtainBanch(t *testing.T) { + type scenario struct { + testName string + input []string + expectedBranch *models.Branch + } + + scenarios := []scenario{ + { + testName: "TrimHeads", + input: []string{"", "heads/a_branch", "", ""}, + expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false}, + }, + { + testName: "NoUpstream", + input: []string{"", "a_branch", "", ""}, + expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false}, + }, + { + testName: "IsHead", + input: []string{"*", "a_branch", "", ""}, + expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true}, + }, + { + testName: "IsBehindAndAhead", + input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"}, + expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false}, + }, + { + testName: "RemoteBranchIsGone", + input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"}, + expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false}, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + branch := obtainBranch(s.input) + assert.EqualValues(t, s.expectedBranch, branch) + }) + } +} diff --git a/pkg/commands/models/branch.go b/pkg/commands/models/branch.go index 3cdf5ad6d..dae934fdf 100644 --- a/pkg/commands/models/branch.go +++ b/pkg/commands/models/branch.go @@ -5,11 +5,12 @@ package models type Branch struct { Name string // the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf' - DisplayName string - Recency string - Pushables string - Pullables string - Head bool + DisplayName string + Recency string + Pushables string + Pullables string + UpstreamGone bool + Head bool // if we have a named remote locally this will be the name of that remote e.g. // 'origin' or 'tiwood'. If we don't have the remote locally it'll look like // 'git@github.com:tiwood/lazygit.git' diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go index 89e1461b8..010a2f0b1 100644 --- a/pkg/gui/list_context_config.go +++ b/pkg/gui/list_context_config.go @@ -47,7 +47,7 @@ func (gui *Gui) branchesListContext() *context.BranchesContext { func() []*models.Branch { return gui.State.Model.Branches }, gui.Views.Branches, func(startIdx int, length int) [][]string { - return presentation.GetBranchListDisplayStrings(gui.State.Model.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref) + return presentation.GetBranchListDisplayStrings(gui.State.Model.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref, gui.Tr) }, nil, OnFocusWrapper(gui.withDiffModeCheck(gui.branchesRenderToMain)), diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index e31c823ce..9062eface 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -6,25 +6,26 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" ) var branchPrefixColorCache = make(map[string]style.TextStyle) -func GetBranchListDisplayStrings(branches []*models.Branch, fullDescription bool, diffName string) [][]string { +func GetBranchListDisplayStrings(branches []*models.Branch, fullDescription bool, diffName string, tr *i18n.TranslationSet) [][]string { lines := make([][]string, len(branches)) for i := range branches { diffed := branches[i].Name == diffName - lines[i] = getBranchDisplayStrings(branches[i], fullDescription, diffed) + lines[i] = getBranchDisplayStrings(branches[i], fullDescription, diffed, tr) } return lines } // getBranchDisplayStrings returns the display string of branch -func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool) []string { +func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool, tr *i18n.TranslationSet) []string { displayName := b.Name if b.DisplayName != "" { displayName = b.DisplayName @@ -36,7 +37,7 @@ func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool } coloredName := nameTextStyle.Sprint(displayName) if b.IsTrackingRemote() { - coloredName = fmt.Sprintf("%s %s", coloredName, ColoredBranchStatus(b)) + coloredName = fmt.Sprintf("%s %s", coloredName, ColoredBranchStatus(b, tr)) } recencyColor := style.FgCyan @@ -77,18 +78,21 @@ func GetBranchTextStyle(name string) style.TextStyle { } } -func ColoredBranchStatus(branch *models.Branch) string { +func ColoredBranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string { colour := style.FgYellow - if branch.MatchesUpstream() { - colour = style.FgGreen - } else if !branch.IsTrackingRemote() { + if !branch.IsTrackingRemote() || branch.UpstreamGone { colour = style.FgRed + } else if branch.MatchesUpstream() { + colour = style.FgGreen } - return colour.Sprint(BranchStatus(branch)) + return colour.Sprint(BranchStatus(branch, tr)) } -func BranchStatus(branch *models.Branch) string { +func BranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string { + if branch.UpstreamGone { + return tr.UpstreamGone + } return fmt.Sprintf("↑%s↓%s", branch.Pushables, branch.Pullables) } diff --git a/pkg/gui/refresh.go b/pkg/gui/refresh.go index d9d661cff..2a47efc7a 100644 --- a/pkg/gui/refresh.go +++ b/pkg/gui/refresh.go @@ -534,7 +534,7 @@ func (gui *Gui) refreshStatus() { status := "" if currentBranch.IsRealBranch() { - status += presentation.ColoredBranchStatus(currentBranch) + " " + status += presentation.ColoredBranchStatus(currentBranch, gui.Tr) + " " } workingTreeState := gui.git.Status.WorkingTreeState() diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 072f41da9..6ca6e6996 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -41,7 +41,7 @@ func (gui *Gui) handleStatusClick() error { } cx, _ := gui.Views.Status.Cursor() - upstreamStatus := presentation.BranchStatus(currentBranch) + upstreamStatus := presentation.BranchStatus(currentBranch, gui.Tr) repoName := utils.GetCurrentRepoName() workingTreeState := gui.git.Status.WorkingTreeState() switch workingTreeState { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index e72fe7d06..4724abcab 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -458,6 +458,7 @@ type TranslationSet struct { RewordInEditorPrompt string CheckoutPrompt string HardResetAutostashPrompt string + UpstreamGone string Actions Actions Bisect Bisect } @@ -1035,6 +1036,7 @@ func EnglishTranslationSet() TranslationSet { RewordInEditorPrompt: "Are you sure you want to reword this commit in your editor?", HardResetAutostashPrompt: "Are you sure you want to hard reset to '%s'? An auto-stash will be performed if necessary.", CheckoutPrompt: "Are you sure you want to checkout '%s'?", + UpstreamGone: "↑gone", Actions: Actions{ // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) CheckoutCommit: "Checkout commit",