mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-11-29 22:48:24 +02:00
Show github pull request status against branch
This commit is contained in:
committed by
Stefan Haller
parent
32a701cb9c
commit
55d2ac6fe7
@@ -126,7 +126,7 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
|
||||
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
|
||||
err = self.gui.git.Sync.FetchBackground()
|
||||
|
||||
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
|
||||
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS, types.PULL_REQUESTS}, Mode: types.SYNC})
|
||||
|
||||
if err == nil {
|
||||
err = self.gui.helpers.BranchesHelper.AutoForwardBranches()
|
||||
|
||||
@@ -28,6 +28,8 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
||||
return presentation.GetBranchListDisplayStrings(
|
||||
viewModel.GetItems(),
|
||||
c.State().GetItemOperation,
|
||||
c.Model().PullRequests,
|
||||
c.Model().Remotes,
|
||||
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
|
||||
c.Modes().Diffing.Ref,
|
||||
c.Views().Branches.InnerWidth()+c.Views().Branches.OriginX(),
|
||||
|
||||
@@ -68,6 +68,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
mergeConflictsHelper,
|
||||
worktreeHelper,
|
||||
searchHelper,
|
||||
suggestionsHelper,
|
||||
)
|
||||
diffHelper := helpers.NewDiffHelper(helperCommon)
|
||||
cherryPickHelper := helpers.NewCherryPickHelper(
|
||||
|
||||
@@ -27,7 +27,6 @@ type Helpers struct {
|
||||
MergeAndRebase *MergeAndRebaseHelper
|
||||
MergeConflicts *MergeConflictsHelper
|
||||
CherryPick *CherryPickHelper
|
||||
Host *HostHelper
|
||||
PatchBuilding *PatchBuildingHelper
|
||||
Staging *StagingHelper
|
||||
GPG *GpgHelper
|
||||
@@ -53,6 +52,7 @@ type Helpers struct {
|
||||
Search *SearchHelper
|
||||
Worktree *WorktreeHelper
|
||||
SubCommits *SubCommitsHelper
|
||||
Host *HostHelper
|
||||
}
|
||||
|
||||
func NewStubHelpers() *Helpers {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -27,6 +28,7 @@ type RefreshHelper struct {
|
||||
mergeConflictsHelper *MergeConflictsHelper
|
||||
worktreeHelper *WorktreeHelper
|
||||
searchHelper *SearchHelper
|
||||
suggestionsHelper *SuggestionsHelper
|
||||
}
|
||||
|
||||
func NewRefreshHelper(
|
||||
@@ -38,6 +40,7 @@ func NewRefreshHelper(
|
||||
mergeConflictsHelper *MergeConflictsHelper,
|
||||
worktreeHelper *WorktreeHelper,
|
||||
searchHelper *SearchHelper,
|
||||
suggestionsHelper *SuggestionsHelper,
|
||||
) *RefreshHelper {
|
||||
return &RefreshHelper{
|
||||
c: c,
|
||||
@@ -48,6 +51,7 @@ func NewRefreshHelper(
|
||||
mergeConflictsHelper: mergeConflictsHelper,
|
||||
worktreeHelper: worktreeHelper,
|
||||
searchHelper: searchHelper,
|
||||
suggestionsHelper: suggestionsHelper,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +95,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
|
||||
types.STATUS,
|
||||
types.BISECT_INFO,
|
||||
types.STAGING,
|
||||
types.PULL_REQUESTS,
|
||||
})
|
||||
} else {
|
||||
scopeSet = set.NewFromSlice(options.Scope)
|
||||
@@ -117,6 +122,10 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.PULL_REQUESTS) {
|
||||
refresh("pull requests", func() { _ = self.refreshGithubPullRequests() })
|
||||
}
|
||||
|
||||
includeWorktreesWithBranches := false
|
||||
if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) {
|
||||
// whenever we change commits, we should update branches because the upstream/downstream
|
||||
@@ -757,3 +766,130 @@ func (self *RefreshHelper) refreshView(context types.Context) {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshGithubPullRequests() error {
|
||||
self.c.Mutexes().RefreshingPullRequestsMutex.Lock()
|
||||
defer self.c.Mutexes().RefreshingPullRequestsMutex.Unlock()
|
||||
|
||||
if !self.c.UserConfig().Git.EnableGithubCli {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !self.c.Git().GitHub.InGithubRepo() {
|
||||
self.c.Model().PullRequests = []*models.GithubPullRequest{}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch self.c.State().GetGitHubCliState() {
|
||||
case types.UNKNOWN:
|
||||
state := self.determineGithubCliState()
|
||||
self.c.State().SetGitHubCliState(state)
|
||||
if state != types.VALID {
|
||||
if state == types.INVALID_VERSION {
|
||||
// todo: i18n
|
||||
self.c.LogAction("gh version is too old (must be version 2 or greater), so pull requests will not be shown against branches.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
case types.VALID:
|
||||
// continue on
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := self.c.Git().GitHub.BaseRepo(); err != nil {
|
||||
ok, err := self.promptForBaseGithubRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.setGithubPullRequests(); err != nil {
|
||||
self.c.LogAction(fmt.Sprintf("Error fetching pull requests from GitHub: %s", err.Error()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) promptForBaseGithubRepo() (bool, error) {
|
||||
err := self.refreshRemotes()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch len(self.c.Model().Remotes) {
|
||||
case 0:
|
||||
return false, nil
|
||||
case 1:
|
||||
remote := self.c.Model().Remotes[0]
|
||||
|
||||
if len(remote.Urls) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
repoName, err := self.c.Git().HostingService.GetRepoNameFromRemoteURL(remote.Urls[0])
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
_, err = self.c.Git().GitHub.SetBaseRepo(repoName)
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
default:
|
||||
self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.SelectRemoteRepository,
|
||||
InitialContent: "",
|
||||
FindSuggestionsFunc: self.suggestionsHelper.GetRemoteRepoSuggestionsFunc(),
|
||||
HandleConfirm: func(repository string) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.LcSelectingRemote, func(gocui.Task) error {
|
||||
// `repository` is something like 'jesseduffield/lazygit'
|
||||
_, err := self.c.Git().GitHub.SetBaseRepo(repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.refreshGithubPullRequests()
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) determineGithubCliState() types.GitHubCliState {
|
||||
installed, validVersion := self.c.Git().GitHub.DetermineGitHubCliState()
|
||||
if validVersion {
|
||||
return types.VALID
|
||||
} else if installed {
|
||||
return types.INVALID_VERSION
|
||||
}
|
||||
return types.NOT_INSTALLED
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) setGithubPullRequests() error {
|
||||
branches := lo.Filter(self.c.Model().Branches, func(branch *models.Branch, _ int) bool {
|
||||
return branch.IsTrackingRemote()
|
||||
})
|
||||
branchNames := lo.Map(branches, func(branch *models.Branch, _ int) string {
|
||||
return branch.UpstreamBranch
|
||||
})
|
||||
|
||||
prs, err := self.c.Git().GitHub.FetchRecentPRs(branchNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.c.Model().PullRequests = prs
|
||||
|
||||
self.c.PostRefreshUpdate(self.c.Contexts().Branches)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -65,6 +65,30 @@ func (self *SuggestionsHelper) getBranchNames() []string {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *SuggestionsHelper) GetRemoteRepoSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
repoNames := self.getRemoteRepoNames()
|
||||
|
||||
return FilterFunc(repoNames, self.c.UserConfig().Gui.UseFuzzySearch())
|
||||
}
|
||||
|
||||
func (self *SuggestionsHelper) getRemoteRepoNames() []string {
|
||||
remotes := self.c.Model().Remotes
|
||||
result := make([]string, 0, len(remotes))
|
||||
for _, remote := range remotes {
|
||||
if len(remote.Urls) == 0 {
|
||||
continue
|
||||
}
|
||||
repoName, err := self.c.Git().HostingService.GetRepoNameFromRemoteURL(remote.Urls[0])
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
continue
|
||||
}
|
||||
result = append(result, repoName)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
branchNames := self.getBranchNames()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
@@ -60,19 +61,5 @@ func (self *UpstreamHelper) PromptForUpstreamWithoutInitialContent(_ *models.Bra
|
||||
}
|
||||
|
||||
func (self *UpstreamHelper) GetSuggestedRemote() string {
|
||||
return getSuggestedRemote(self.c.Model().Remotes)
|
||||
}
|
||||
|
||||
func getSuggestedRemote(remotes []*models.Remote) string {
|
||||
if len(remotes) == 0 {
|
||||
return "origin"
|
||||
}
|
||||
|
||||
for _, remote := range remotes {
|
||||
if remote.Name == "origin" {
|
||||
return remote.Name
|
||||
}
|
||||
}
|
||||
|
||||
return remotes[0].Name
|
||||
return git_commands.GetSuggestedRemoteName(self.c.Model().Remotes)
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetSuggestedRemote(t *testing.T) {
|
||||
cases := []struct {
|
||||
remotes []*models.Remote
|
||||
expected string
|
||||
}{
|
||||
{mkRemoteList(), "origin"},
|
||||
{mkRemoteList("upstream", "origin", "foo"), "origin"},
|
||||
{mkRemoteList("upstream", "foo", "bar"), "upstream"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
result := getSuggestedRemote(c.remotes)
|
||||
assert.EqualValues(t, c.expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func mkRemoteList(names ...string) []*models.Remote {
|
||||
return lo.Map(names, func(name string, _ int) *models.Remote {
|
||||
return &models.Remote{Name: name}
|
||||
})
|
||||
}
|
||||
@@ -147,6 +147,7 @@ type Gui struct {
|
||||
integrationTest integrationTypes.IntegrationTest
|
||||
|
||||
afterLayoutFuncs chan func() error
|
||||
gitHubCliState types.GitHubCliState
|
||||
}
|
||||
|
||||
type StateAccessor struct {
|
||||
@@ -220,6 +221,14 @@ func (self *StateAccessor) ClearItemOperation(item types.HasUrn) {
|
||||
delete(self.gui.itemOperations, item.URN())
|
||||
}
|
||||
|
||||
func (self *StateAccessor) GetGitHubCliState() types.GitHubCliState {
|
||||
return self.gui.gitHubCliState
|
||||
}
|
||||
|
||||
func (self *StateAccessor) SetGitHubCliState(value types.GitHubCliState) {
|
||||
self.gui.gitHubCliState = value
|
||||
}
|
||||
|
||||
// we keep track of some stuff from one render to the next to see if certain
|
||||
// things have changed
|
||||
type PrevLayout struct {
|
||||
@@ -569,6 +578,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
|
||||
Authors: map[string]*models.Author{},
|
||||
MainBranches: git_commands.NewMainBranches(gui.c.Common, gui.os.Cmd),
|
||||
HashPool: &utils.StringPool{},
|
||||
PullRequests: make([]*models.GithubPullRequest, 0),
|
||||
},
|
||||
Modes: &types.Modes{
|
||||
Filtering: filtering.New(startArgs.FilterPath, ""),
|
||||
|
||||
@@ -29,6 +29,8 @@ var colorPatterns *colorMatcher
|
||||
func GetBranchListDisplayStrings(
|
||||
branches []*models.Branch,
|
||||
getItemOperation func(item types.HasUrn) types.ItemOperation,
|
||||
pullRequests []*models.GithubPullRequest,
|
||||
remotes []*models.Remote,
|
||||
fullDescription bool,
|
||||
diffName string,
|
||||
viewWidth int,
|
||||
@@ -36,9 +38,15 @@ func GetBranchListDisplayStrings(
|
||||
userConfig *config.UserConfig,
|
||||
worktrees []*models.Worktree,
|
||||
) [][]string {
|
||||
prs := git_commands.GenerateGithubPullRequestMap(
|
||||
pullRequests,
|
||||
branches,
|
||||
remotes,
|
||||
)
|
||||
|
||||
return lo.Map(branches, func(branch *models.Branch, _ int) []string {
|
||||
diffed := branch.Name == diffName
|
||||
return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, viewWidth, tr, userConfig, worktrees, time.Now())
|
||||
return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, viewWidth, tr, userConfig, worktrees, time.Now(), prs)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,6 +61,7 @@ func getBranchDisplayStrings(
|
||||
userConfig *config.UserConfig,
|
||||
worktrees []*models.Worktree,
|
||||
now time.Time,
|
||||
prs map[string]*models.GithubPullRequest,
|
||||
) []string {
|
||||
checkedOutByWorkTree := git_commands.CheckedOutByOtherWorktree(b, worktrees)
|
||||
showCommitHash := fullDescription || userConfig.Gui.ShowBranchCommitHash
|
||||
@@ -101,6 +110,7 @@ func getBranchDisplayStrings(
|
||||
if checkedOutByWorkTree {
|
||||
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
|
||||
}
|
||||
|
||||
if len(branchStatus) > 0 {
|
||||
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
|
||||
}
|
||||
@@ -111,14 +121,22 @@ func getBranchDisplayStrings(
|
||||
}
|
||||
|
||||
res := make([]string, 0, 6)
|
||||
|
||||
res = append(res, recencyColor.Sprint(b.Recency))
|
||||
|
||||
if icons.IsIconEnabled() {
|
||||
res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
|
||||
}
|
||||
|
||||
if showCommitHash {
|
||||
res = append(res, utils.ShortHash(b.CommitHash))
|
||||
pr, hasPr := prs[b.Name]
|
||||
if hasPr {
|
||||
if icons.IsIconEnabled() {
|
||||
res = append(res, prColor(pr.State).Sprint(icons.IconForBranch(b)))
|
||||
} else {
|
||||
res = append(res, prColor(pr.State).Sprint("⬤"))
|
||||
}
|
||||
} else {
|
||||
if icons.IsIconEnabled() {
|
||||
res = append(res, style.FgDefault.Sprint(icons.IconForBranch(b)))
|
||||
} else {
|
||||
res = append(res, style.FgDefault.Sprint("⬤"))
|
||||
}
|
||||
}
|
||||
|
||||
if divergence != "" {
|
||||
@@ -128,8 +146,13 @@ func getBranchDisplayStrings(
|
||||
coloredName += style.FgCyan.Sprint(divergence)
|
||||
}
|
||||
}
|
||||
|
||||
res = append(res, coloredName)
|
||||
|
||||
if showCommitHash {
|
||||
res = append(res, utils.ShortHash(b.CommitHash))
|
||||
}
|
||||
|
||||
if fullDescription {
|
||||
res = append(
|
||||
res,
|
||||
@@ -229,3 +252,24 @@ func SetCustomBranches(customBranchColors map[string]string, isRegex bool) {
|
||||
isRegex: isRegex,
|
||||
}
|
||||
}
|
||||
|
||||
// func coloredPrNumber(pr *models.GithubPullRequest, hasPr bool) string {
|
||||
// if hasPr {
|
||||
// return prColor(pr.State).Sprint("#" + strconv.Itoa(pr.Number))
|
||||
// }
|
||||
|
||||
// return ("")
|
||||
// }
|
||||
|
||||
func prColor(state string) style.TextStyle {
|
||||
switch state {
|
||||
case "OPEN":
|
||||
return style.FgGreen
|
||||
case "CLOSED":
|
||||
return style.FgRed
|
||||
case "MERGED":
|
||||
return style.FgMagenta
|
||||
default:
|
||||
return style.FgDefault
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run(fmt.Sprintf("getBranchDisplayStrings_%d", i), func(t *testing.T) {
|
||||
strings := getBranchDisplayStrings(s.branch, s.itemOperation, s.fullDescription, false, s.viewWidth, c.Tr, c.UserConfig(), worktrees, time.Time{})
|
||||
strings := getBranchDisplayStrings(s.branch, s.itemOperation, s.fullDescription, false, s.viewWidth, c.Tr, c.UserConfig(), worktrees, time.Time{}, map[string]*models.GithubPullRequest{})
|
||||
assert.Equal(t, s.expected, strings)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -292,6 +292,7 @@ type Model struct {
|
||||
SubCommits []*models.Commit
|
||||
Remotes []*models.Remote
|
||||
Worktrees []*models.Worktree
|
||||
PullRequests []*models.GithubPullRequest
|
||||
|
||||
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
||||
// When in filtering mode we only include the ones that match the given path
|
||||
@@ -322,15 +323,16 @@ type Model struct {
|
||||
}
|
||||
|
||||
type Mutexes struct {
|
||||
RefreshingFilesMutex deadlock.Mutex
|
||||
RefreshingBranchesMutex deadlock.Mutex
|
||||
RefreshingStatusMutex deadlock.Mutex
|
||||
LocalCommitsMutex deadlock.Mutex
|
||||
SubCommitsMutex deadlock.Mutex
|
||||
AuthorsMutex deadlock.Mutex
|
||||
SubprocessMutex deadlock.Mutex
|
||||
PopupMutex deadlock.Mutex
|
||||
PtyMutex deadlock.Mutex
|
||||
RefreshingFilesMutex deadlock.Mutex
|
||||
RefreshingBranchesMutex deadlock.Mutex
|
||||
RefreshingStatusMutex deadlock.Mutex
|
||||
RefreshingPullRequestsMutex deadlock.Mutex
|
||||
LocalCommitsMutex deadlock.Mutex
|
||||
SubCommitsMutex deadlock.Mutex
|
||||
AuthorsMutex deadlock.Mutex
|
||||
SubprocessMutex deadlock.Mutex
|
||||
PopupMutex deadlock.Mutex
|
||||
PtyMutex deadlock.Mutex
|
||||
}
|
||||
|
||||
// A long-running operation associated with an item. For example, we'll show
|
||||
@@ -369,6 +371,8 @@ type IStateAccessor interface {
|
||||
GetItemOperation(item HasUrn) ItemOperation
|
||||
SetItemOperation(item HasUrn, operation ItemOperation)
|
||||
ClearItemOperation(item HasUrn)
|
||||
GetGitHubCliState() GitHubCliState
|
||||
SetGitHubCliState(GitHubCliState)
|
||||
}
|
||||
|
||||
type IRepoStateAccessor interface {
|
||||
@@ -405,3 +409,13 @@ const (
|
||||
SCREEN_HALF
|
||||
SCREEN_FULL
|
||||
)
|
||||
|
||||
// for keeping track of whether our github CLI is installed and on a valid version
|
||||
type GitHubCliState int
|
||||
|
||||
const (
|
||||
UNKNOWN GitHubCliState = iota
|
||||
VALID
|
||||
NOT_INSTALLED
|
||||
INVALID_VERSION
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
COMMIT_FILES
|
||||
// not actually a view. Will refactor this later
|
||||
BISECT_INFO
|
||||
PULL_REQUESTS
|
||||
)
|
||||
|
||||
type RefreshMode int
|
||||
|
||||
Reference in New Issue
Block a user