1
0
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:
Jesse Duffield
2024-06-03 22:12:09 +10:00
committed by Stefan Haller
parent 32a701cb9c
commit 55d2ac6fe7
42 changed files with 1992 additions and 90 deletions

View File

@@ -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()

View File

@@ -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(),

View File

@@ -68,6 +68,7 @@ func (gui *Gui) resetHelpersAndControllers() {
mergeConflictsHelper,
worktreeHelper,
searchHelper,
suggestionsHelper,
)
diffHelper := helpers.NewDiffHelper(helperCommon)
cherryPickHelper := helpers.NewCherryPickHelper(

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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}
})
}

View File

@@ -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, ""),

View File

@@ -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
}
}

View File

@@ -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)
})
}

View File

@@ -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
)

View File

@@ -22,6 +22,7 @@ const (
COMMIT_FILES
// not actually a view. Will refactor this later
BISECT_INFO
PULL_REQUESTS
)
type RefreshMode int