From f277ea235002ef14aac6ff5900fbd9cd9d3cbda7 Mon Sep 17 00:00:00 2001 From: Yuki Osaki Date: Mon, 15 Aug 2022 15:23:24 +0900 Subject: [PATCH] fix conflict --- docs/keybindings/Keybindings_en.md | 70 +----- docs/keybindings/Keybindings_zh.md | 2 +- pkg/app/app.go | 23 ++ pkg/commands/git_commands/gh.go | 33 +-- pkg/config/user_config.go | 52 ++--- pkg/gui/branches_panel.go | 45 +++- .../controllers/helpers/suggestions_helper.go | 21 ++ pkg/gui/find_suggestions.go | 211 ------------------ pkg/gui/gui.go | 17 +- pkg/gui/list_context_config.go | 3 +- pkg/gui/presentation/branches.go | 18 +- pkg/gui/pull_request_menu_panel.go | 94 -------- pkg/gui/refresh.go | 4 + pkg/gui/types/common.go | 1 + pkg/i18n/chinese.go | 4 +- pkg/i18n/dutch.go | 2 +- pkg/i18n/english.go | 25 +-- pkg/i18n/polish.go | 4 +- 18 files changed, 181 insertions(+), 448 deletions(-) delete mode 100644 pkg/gui/find_suggestions.go delete mode 100644 pkg/gui/pull_request_menu_panel.go diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index d9884256b..af5787dab 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -41,75 +41,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct [: previous tab -## Branches Panel (Branches Tab) - -
-  space: checkout
-  o: create pull request
-  O: create pull request options
-  ctrl+y: copy pull request URL to clipboard
-  c: checkout by name
-  F: force checkout
-  n: new branch
-  d: delete branch
-  r: rebase checked-out branch onto this branch
-  M: merge into currently checked out branch
-  i: show git-flow options
-  f: fast-forward this branch from its upstream
-  g: view reset options
-  R: rename branch
-  ctrl+o: copy branch name to clipboard
-  enter: view commits
-
- -## Branches Panel (Remote Branches (in Remotes tab)) - -
-  esc: Return to remotes list
-  g: view reset options
-  enter: view commits
-  space: checkout
-  n: new branch
-  M: merge into currently checked out branch
-  d: delete branch
-  r: rebase checked-out branch onto this branch
-  u: set as upstream of checked-out branch
-
- -## Branches Panel (Remotes Tab) - -
-  f: fetch remote
-  n: add new remote
-  d: remove remote
-  e: edit remote
-
- -## Branches Panel (Sub-commits) - -
-  enter: view commit's files
-  space: checkout commit
-  g: view reset options
-  n: new branch
-  c: copy commit (cherry-pick)
-  C: copy commit range (cherry-pick)
-  ctrl+r: reset cherry-picked (copied) commits selection
-  ctrl+o: copy commit SHA to clipboard
-
- -## Branches Panel (Tags Tab) - -
-  space: checkout
-  d: delete tag
-  P: push tag
-  n: create tag
-  g: view reset options
-  enter: view commits
-
- -## Commit Files Panel +## Commit Files
   ctrl+o: copy the committed file name to the clipboard
diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md
index 7ffaf2292..df1db2e55 100644
--- a/docs/keybindings/Keybindings_zh.md
+++ b/docs/keybindings/Keybindings_zh.md
@@ -64,7 +64,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   space: 检出
   n: 新分支
   o: 创建抓取请求
-  O: 创建抓取请求
+  O: 创建抓取请求选项
   ctrl+y: 将抓取请求 URL 复制到剪贴板
   c: 按名称检出
   F: 强制检出
diff --git a/pkg/app/app.go b/pkg/app/app.go
index a1b53d96c..88af18486 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -136,6 +136,29 @@ func (app *App) validateGhVersion() error {
 	return minVersionError
 }
 
+func isGhVersionValid(versionStr string) bool {
+	// output should be something like:
+	// gh version 2.0.0 (2021-08-23)
+	// https://github.com/cli/cli/releases/tag/v2.0.0
+	re := regexp.MustCompile(`[^\d]+([\d\.]+)`)
+	matches := re.FindStringSubmatch(versionStr)
+
+	if len(matches) == 0 {
+		return false
+	}
+
+	ghVersion := matches[1]
+	majorVersion, err := strconv.Atoi(ghVersion[0:1])
+	if err != nil {
+		return false
+	}
+	if majorVersion < 2 {
+		return false
+	}
+
+	return true
+}
+
 func (app *App) validateGitVersion() error {
 	output, err := app.OSCommand.Cmd.New("git --version").RunWithOutput()
 	// if we get an error anywhere here we'll show the same status
diff --git a/pkg/commands/git_commands/gh.go b/pkg/commands/git_commands/gh.go
index 30d580aa4..2edd36a73 100644
--- a/pkg/commands/git_commands/gh.go
+++ b/pkg/commands/git_commands/gh.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
 	"github.com/jesseduffield/lazygit/pkg/commands/models"
 )
 
@@ -21,17 +20,16 @@ func NewGhCommand(gitCommon *GitCommon) *GhCommands {
 
 // https://github.com/cli/cli/issues/2300
 func (self *GhCommands) BaseRepo() error {
-	return self.cmd.New("git config --local --get-regexp .gh-resolved").StreamOutput().Run()
-
+	return self.cmd.New("git config --local --get-regexp .gh-resolved").Run()
 }
 
 // Ex: git config --local --add "remote.origin.gh-resolved" "jesseduffield/lazygit"
 func (self *GhCommands) SetBaseRepo(repository string) (string, error) {
-	return self.cmd.NewShell(fmt.Sprintf("git config --local --add \"remote.origin.gh-resolved\" \"%s\"", repository)).RunWithOutput()
+	return self.cmd.New(fmt.Sprintf("git config --local --add \"remote.origin.gh-resolved\" \"%s\"", repository)).RunWithOutput()
 }
 
 func (self *GhCommands) prList() (string, error) {
-	return self.cmd.NewShell("gh pr list --limit 100 --state all --json state,url,number,headRefName,headRepositoryOwner").RunWithOutput()
+	return self.cmd.New("gh pr list --limit 500 --state all --json state,url,number,headRefName,headRepositoryOwner").RunWithOutput()
 }
 
 func (self *GhCommands) GithubMostRecentPRs() ([]*models.GithubPullRequest, error) {
@@ -49,17 +47,17 @@ func (self *GhCommands) GithubMostRecentPRs() ([]*models.GithubPullRequest, erro
 	return prs, nil
 }
 
-func GenerateGithubPullRequestMap(prs []*models.GithubPullRequest, branches []*models.Branch, remotes []*models.Remote) (map[*models.Branch]*models.GithubPullRequest, error) {
+func GenerateGithubPullRequestMap(prs []*models.GithubPullRequest, branches []*models.Branch, remotes []*models.Remote) map[*models.Branch]*models.GithubPullRequest {
 	res := map[*models.Branch]*models.GithubPullRequest{}
 
 	if len(prs) == 0 {
-		return res, nil
+		return res
 	}
 
-	remotesToOwnersMap, err := getRemotesToOwnersMap(remotes)
+	remotesToOwnersMap := getRemotesToOwnersMap(remotes)
 
 	if len(remotesToOwnersMap) == 0 {
-		return res, err
+		return res
 	}
 
 	prWithStringKey := map[string]models.GithubPullRequest{}
@@ -87,10 +85,10 @@ func GenerateGithubPullRequestMap(prs []*models.GithubPullRequest, branches []*m
 		res[branch] = &pr
 	}
 
-	return res, nil
+	return res
 }
 
-func GetRepoInfoFromURL(url string) hosting_service.RepoInformation {
+func GetRepoInfoFromURL(url string) RepoInformation {
 	isHTTP := strings.HasPrefix(url, "http")
 
 	if isHTTP {
@@ -98,7 +96,7 @@ func GetRepoInfoFromURL(url string) hosting_service.RepoInformation {
 		owner := strings.Join(splits[3:len(splits)-1], "/")
 		repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
 
-		return hosting_service.RepoInformation{
+		return RepoInformation{
 			Owner:      owner,
 			Repository: repo,
 		}
@@ -109,13 +107,13 @@ func GetRepoInfoFromURL(url string) hosting_service.RepoInformation {
 	owner := strings.Join(splits[0:len(splits)-1], "/")
 	repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
 
-	return hosting_service.RepoInformation{
+	return RepoInformation{
 		Owner:      owner,
 		Repository: repo,
 	}
 }
 
-func getRemotesToOwnersMap(remotes []*models.Remote) (map[string]string, error) {
+func getRemotesToOwnersMap(remotes []*models.Remote) map[string]string {
 	res := map[string]string{}
 	for _, remote := range remotes {
 		if len(remote.Urls) == 0 {
@@ -124,5 +122,10 @@ func getRemotesToOwnersMap(remotes []*models.Remote) (map[string]string, error)
 
 		res[remote.Name] = GetRepoInfoFromURL(remote.Urls[0]).Owner
 	}
-	return res, nil
+	return res
+}
+
+type RepoInformation struct {
+	Owner      string
+	Repository string
 }
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index 05020a3fa..f6f9621d1 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -224,19 +224,19 @@ type KeybindingFilesConfig struct {
 }
 
 type KeybindingBranchesConfig struct {
-	CreateOrShowPullRequest string `yaml:"createPullRequest"`
-	ViewPullRequestOptions  string `yaml:"viewPullRequestOptions"`
-	CopyPullRequestURL      string `yaml:"copyPullRequestURL"`
-	CheckoutBranchByName    string `yaml:"checkoutBranchByName"`
-	ForceCheckoutBranch     string `yaml:"forceCheckoutBranch"`
-	RebaseBranch            string `yaml:"rebaseBranch"`
-	RenameBranch            string `yaml:"renameBranch"`
-	MergeIntoCurrentBranch  string `yaml:"mergeIntoCurrentBranch"`
-	ViewGitFlowOptions      string `yaml:"viewGitFlowOptions"`
-	FastForward             string `yaml:"fastForward"`
-	PushTag                 string `yaml:"pushTag"`
-	SetUpstream             string `yaml:"setUpstream"`
-	FetchRemote             string `yaml:"fetchRemote"`
+	CreatePullRequest      string `yaml:"createPullRequest"`
+	ViewPullRequestOptions string `yaml:"viewPullRequestOptions"`
+	CopyPullRequestURL     string `yaml:"copyPullRequestURL"`
+	CheckoutBranchByName   string `yaml:"checkoutBranchByName"`
+	ForceCheckoutBranch    string `yaml:"forceCheckoutBranch"`
+	RebaseBranch           string `yaml:"rebaseBranch"`
+	RenameBranch           string `yaml:"renameBranch"`
+	MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
+	ViewGitFlowOptions     string `yaml:"viewGitFlowOptions"`
+	FastForward            string `yaml:"fastForward"`
+	PushTag                string `yaml:"pushTag"`
+	SetUpstream            string `yaml:"setUpstream"`
+	FetchRemote            string `yaml:"fetchRemote"`
 }
 
 type KeybindingCommitsConfig struct {
@@ -505,19 +505,19 @@ func GetDefaultConfig() *UserConfig {
 				OpenStatusFilter:         "",
 			},
 			Branches: KeybindingBranchesConfig{
-				CopyPullRequestURL:      "",
-				CreateOrShowPullRequest: "o",
-				ViewPullRequestOptions:  "O",
-				CheckoutBranchByName:    "c",
-				ForceCheckoutBranch:     "F",
-				RebaseBranch:            "r",
-				RenameBranch:            "R",
-				MergeIntoCurrentBranch:  "M",
-				ViewGitFlowOptions:      "i",
-				FastForward:             "f",
-				PushTag:                 "P",
-				SetUpstream:             "u",
-				FetchRemote:             "f",
+				CopyPullRequestURL:     "",
+				CreatePullRequest:      "o",
+				ViewPullRequestOptions: "O",
+				CheckoutBranchByName:   "c",
+				ForceCheckoutBranch:    "F",
+				RebaseBranch:           "r",
+				RenameBranch:           "R",
+				MergeIntoCurrentBranch: "M",
+				ViewGitFlowOptions:     "i",
+				FastForward:            "f",
+				PushTag:                "P",
+				SetUpstream:            "u",
+				FetchRemote:            "f",
 			},
 			Commits: KeybindingCommitsConfig{
 				SquashDown:                     "s",
diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go
index b9a25ea67..ad3fc1947 100644
--- a/pkg/gui/branches_panel.go
+++ b/pkg/gui/branches_panel.go
@@ -1,6 +1,8 @@
 package gui
 
-import "github.com/jesseduffield/lazygit/pkg/gui/types"
+import (
+	"github.com/jesseduffield/lazygit/pkg/gui/types"
+)
 
 func (gui *Gui) branchesRenderToMain() error {
 	var task types.UpdateTask
@@ -21,3 +23,44 @@ func (gui *Gui) branchesRenderToMain() error {
 		},
 	})
 }
+
+func (gui *Gui) refreshGithubPullRequests() {
+	if err := gui.git.Gh.BaseRepo(); err == nil {
+		_ = gui.setGithubPullRequests()
+		return
+	}
+
+	// when config not exits
+	_ = gui.refreshRemotes()
+
+	_ = gui.c.Prompt(types.PromptOpts{
+		Title:               gui.c.Tr.SelectRemoteRepository,
+		InitialContent:      "",
+		FindSuggestionsFunc: gui.helpers.Suggestions.GetRemoteRepoSuggestionsFunc(),
+		HandleConfirm: func(repository string) error {
+			return gui.c.WithWaitingStatus(gui.c.Tr.LcSelectingRemote, func() error {
+				_, err := gui.git.Gh.SetBaseRepo(repository)
+				if err != nil {
+					return err
+				}
+
+				err = gui.setGithubPullRequests()
+				if err != nil {
+					return err
+				}
+				_ = gui.postRefreshUpdate(gui.State.Contexts.Branches)
+				return nil
+			})
+		},
+	})
+}
+
+func (gui *Gui) setGithubPullRequests() error {
+	prs, err := gui.git.Gh.GithubMostRecentPRs()
+	if err != nil {
+		return gui.c.Error(err)
+	}
+
+	gui.State.Model.PullRequests = prs
+	return nil
+}
diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go
index 0cc4a642b..68816ce9f 100644
--- a/pkg/gui/controllers/helpers/suggestions_helper.go
+++ b/pkg/gui/controllers/helpers/suggestions_helper.go
@@ -5,6 +5,7 @@ import (
 	"os"
 
 	"github.com/jesseduffield/generics/slices"
+	"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
 	"github.com/jesseduffield/lazygit/pkg/commands/models"
 	"github.com/jesseduffield/lazygit/pkg/gui/presentation"
 	"github.com/jesseduffield/lazygit/pkg/gui/types"
@@ -80,6 +81,26 @@ func (self *SuggestionsHelper) getBranchNames() []string {
 	})
 }
 
+func (self *SuggestionsHelper) GetRemoteRepoSuggestionsFunc() func(string) []*types.Suggestion {
+	remotesNames := self.getRemoteRepoNames()
+
+	return FuzzySearchFunc(remotesNames)
+}
+
+func (self *SuggestionsHelper) getRemoteRepoNames() []string {
+	remotes := self.model.Remotes
+	result := make([]string, 0, len(remotes))
+	for _, remote := range remotes {
+		if len(remote.Urls) == 0 {
+			continue
+		}
+		info := git_commands.GetRepoInfoFromURL(remote.Urls[0])
+		result = append(result, fmt.Sprintf("%s/%s", info.Owner, info.Repository))
+	}
+
+	return result
+}
+
 func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
 	branchNames := self.getBranchNames()
 
diff --git a/pkg/gui/find_suggestions.go b/pkg/gui/find_suggestions.go
deleted file mode 100644
index 56a27ee94..000000000
--- a/pkg/gui/find_suggestions.go
+++ /dev/null
@@ -1,211 +0,0 @@
-package gui
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
-	"github.com/jesseduffield/lazygit/pkg/gui/presentation"
-	"github.com/jesseduffield/lazygit/pkg/gui/types"
-	"github.com/jesseduffield/lazygit/pkg/utils"
-	"github.com/jesseduffield/minimal/gitignore"
-	"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
-)
-
-// Thinking out loud: I'm typically a staunch advocate of organising code by feature rather than type,
-// because colocating code that relates to the same feature means far less effort
-// to get all the context you need to work on any particular feature. But the one
-// major benefit of grouping by type is that it makes it makes it less likely that
-// somebody will re-implement the same logic twice, because they can quickly see
-// if a certain method has been used for some use case, given that as a starting point
-// they know about the type. In that vein, I'm including all our functions for
-// finding suggestions in this file, so that it's easy to see if a function already
-// exists for fetching a particular model.
-
-func (gui *Gui) getRemoteNames() []string {
-	result := make([]string, len(gui.State.Remotes))
-	for i, remote := range gui.State.Remotes {
-		result[i] = remote.Name
-	}
-	return result
-}
-
-func matchesToSuggestions(matches []string) []*types.Suggestion {
-	suggestions := make([]*types.Suggestion, len(matches))
-	for i, match := range matches {
-		suggestions[i] = &types.Suggestion{
-			Value: match,
-			Label: match,
-		}
-	}
-	return suggestions
-}
-
-func (gui *Gui) getRemoteSuggestionsFunc() func(string) []*types.Suggestion {
-	remoteNames := gui.getRemoteNames()
-
-	return fuzzySearchFunc(remoteNames)
-}
-
-func (gui *Gui) getRemoteRepoSuggestionsFunc() func(string) []*types.Suggestion {
-	remotesNames := gui.getRemoteRepoNames()
-
-	return fuzzySearchFunc(remotesNames)
-}
-
-func (gui *Gui) getRemoteRepoNames() []string {
-	remotes := gui.State.Remotes
-	result := make([]string, 0, len(remotes))
-	for _, remote := range remotes {
-		if len(remote.Urls) == 0 {
-			continue
-		}
-		info := git_commands.GetRepoInfoFromURL(remote.Urls[0])
-		result = append(result, fmt.Sprintf("%s/%s", info.Owner, info.Repository))
-	}
-
-	return result
-}
-
-func (gui *Gui) getBranchNames() []string {
-	result := make([]string, len(gui.State.Branches))
-	for i, branch := range gui.State.Branches {
-		result[i] = branch.Name
-	}
-	return result
-}
-
-func (gui *Gui) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
-	branchNames := gui.getBranchNames()
-
-	return func(input string) []*types.Suggestion {
-		var matchingBranchNames []string
-		if input == "" {
-			matchingBranchNames = branchNames
-		} else {
-			matchingBranchNames = utils.FuzzySearch(input, branchNames)
-		}
-
-		suggestions := make([]*types.Suggestion, len(matchingBranchNames))
-		for i, branchName := range matchingBranchNames {
-			suggestions[i] = &types.Suggestion{
-				Value: branchName,
-				Label: presentation.GetBranchTextStyle(branchName).Sprint(branchName),
-			}
-		}
-
-		return suggestions
-	}
-}
-
-// here we asynchronously fetch the latest set of paths in the repo and store in
-// gui.State.FilesTrie. On the main thread we'll be doing a fuzzy search via
-// gui.State.FilesTrie. So if we've looked for a file previously, we'll start with
-// the old trie and eventually it'll be swapped out for the new one.
-// Notably, unlike other suggestion functions we're not showing all the options
-// if nothing has been typed because there'll be too much to display efficiently
-func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
-	_ = gui.WithWaitingStatus(gui.Tr.LcLoadingFileSuggestions, func() error {
-		trie := patricia.NewTrie()
-		// load every non-gitignored file in the repo
-		ignore, err := gitignore.FromGit()
-		if err != nil {
-			return err
-		}
-
-		err = ignore.Walk(".",
-			func(path string, info os.FileInfo, err error) error {
-				if err != nil {
-					return err
-				}
-				trie.Insert(patricia.Prefix(path), path)
-				return nil
-			})
-		// cache the trie for future use
-		gui.State.FilesTrie = trie
-
-		// refresh the selections view
-		gui.suggestionsAsyncHandler.Do(func() func() {
-			// assuming here that the confirmation view is what we're typing into.
-			// This assumption may prove false over time
-			suggestions := gui.findSuggestions(gui.Views.Confirmation.TextArea.GetContent())
-			return func() { gui.setSuggestions(suggestions) }
-		})
-
-		return err
-	})
-
-	return func(input string) []*types.Suggestion {
-		matchingNames := []string{}
-		_ = gui.State.FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error {
-			matchingNames = append(matchingNames, item.(string))
-			return nil
-		})
-
-		// doing another fuzzy search for good measure
-		matchingNames = utils.FuzzySearch(input, matchingNames)
-
-		suggestions := make([]*types.Suggestion, len(matchingNames))
-		for i, name := range matchingNames {
-			suggestions[i] = &types.Suggestion{
-				Value: name,
-				Label: name,
-			}
-		}
-
-		return suggestions
-	}
-}
-
-func (gui *Gui) getRemoteBranchNames(separator string) []string {
-	result := []string{}
-	for _, remote := range gui.State.Remotes {
-		for _, branch := range remote.Branches {
-			result = append(result, fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name))
-		}
-	}
-	return result
-}
-
-func (gui *Gui) getRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
-	return fuzzySearchFunc(gui.getRemoteBranchNames(separator))
-}
-
-func (gui *Gui) getTagNames() []string {
-	result := make([]string, len(gui.State.Tags))
-	for i, tag := range gui.State.Tags {
-		result[i] = tag.Name
-	}
-	return result
-}
-
-func (gui *Gui) getRefsSuggestionsFunc() func(string) []*types.Suggestion {
-	remoteBranchNames := gui.getRemoteBranchNames("/")
-	localBranchNames := gui.getBranchNames()
-	tagNames := gui.getTagNames()
-	additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"}
-
-	refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...)
-
-	return fuzzySearchFunc(refNames)
-}
-
-func (gui *Gui) getCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
-	// reversing so that we display the latest command first
-	history := utils.Reverse(gui.Config.GetAppState().CustomCommandsHistory)
-
-	return fuzzySearchFunc(history)
-}
-
-func fuzzySearchFunc(options []string) func(string) []*types.Suggestion {
-	return func(input string) []*types.Suggestion {
-		var matches []string
-		if input == "" {
-			matches = options
-		} else {
-			matches = utils.FuzzySearch(input, options)
-		}
-
-		return matchesToSuggestions(matches)
-	}
-}
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index d7103914d..2643cac2c 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -238,10 +238,6 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
 	return nil
 }
 
-type GithubState struct {
-	RecentPRs []*models.GithubPullRequest
-}
-
 // reuseState determines if we pull the repo state from our repo state map or
 // just re-initialize it. For now we're only re-using state when we're going
 // in and out of submodules, for the sake of having the cursor back on the submodule
@@ -290,6 +286,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) {
 			ReflogCommits:         make([]*models.Commit, 0),
 			BisectInfo:            git_commands.NewNullBisectInfo(),
 			FilesTrie:             patricia.NewTrie(),
+			PullRequests:          make([]*models.GithubPullRequest, 0),
 		},
 		Modes: &types.Modes{
 			Filtering:     filtering.New(startArgs.FilterPath),
@@ -777,3 +774,15 @@ func (gui *Gui) onUIThread(f func() error) {
 		return f()
 	})
 }
+
+func (gui *Gui) GetPr(branch *models.Branch) (*models.GithubPullRequest, bool, error) {
+	prs := git_commands.GenerateGithubPullRequestMap(
+		gui.State.Model.PullRequests,
+		[]*models.Branch{branch},
+		gui.State.Model.Remotes,
+	)
+
+	pr, hasPr := prs[branch]
+
+	return pr, hasPr, nil
+}
diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go
index d463c2ac5..7e73cd1bb 100644
--- a/pkg/gui/list_context_config.go
+++ b/pkg/gui/list_context_config.go
@@ -45,7 +45,8 @@ 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, gui.Tr)
+			prs := git_commands.GenerateGithubPullRequestMap(gui.State.Model.PullRequests, gui.State.Model.Branches, gui.State.Model.Remotes)
+			return presentation.GetBranchListDisplayStrings(gui.State.Model.Branches, prs, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref, gui.Tr)
 		},
 		nil,
 		gui.withDiffModeCheck(gui.branchesRenderToMain),
diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go
index 1b9cc9b12..ed1d6edda 100644
--- a/pkg/gui/presentation/branches.go
+++ b/pkg/gui/presentation/branches.go
@@ -16,15 +16,17 @@ import (
 
 var branchPrefixColorCache = make(map[string]style.TextStyle)
 
-func GetBranchListDisplayStrings(branches []*models.Branch, fullDescription bool, diffName string, tr *i18n.TranslationSet) [][]string {
+func GetBranchListDisplayStrings(branches []*models.Branch, prs map[*models.Branch]*models.GithubPullRequest, fullDescription bool, diffName string, tr *i18n.TranslationSet) [][]string {
 	return slices.Map(branches, func(branch *models.Branch) []string {
 		diffed := branch.Name == diffName
-		return getBranchDisplayStrings(branch, fullDescription, diffed, tr)
+		return getBranchDisplayStrings(branch, prs, fullDescription, diffed, tr)
 	})
 }
 
 // getBranchDisplayStrings returns the display string of branch
-func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool, tr *i18n.TranslationSet) []string {
+func getBranchDisplayStrings(b *models.Branch,
+	prs map[*models.Branch]*models.GithubPullRequest, fullDescription bool, diffed bool, tr *i18n.TranslationSet,
+) []string {
 	displayName := b.Name
 	if b.DisplayName != "" {
 		displayName = b.DisplayName
@@ -49,7 +51,15 @@ func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool
 	if icons.IsIconEnabled() {
 		res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
 	}
-	res = append(res, coloredName)
+
+	pr, hasPr := prs[b]
+
+	if hasPr {
+		res = append(res, coloredPrNumber(pr, hasPr), coloredName)
+	} else {
+		res = append(res, coloredName)
+	}
+
 	if fullDescription {
 		res = append(
 			res,
diff --git a/pkg/gui/pull_request_menu_panel.go b/pkg/gui/pull_request_menu_panel.go
deleted file mode 100644
index 276074072..000000000
--- a/pkg/gui/pull_request_menu_panel.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package gui
-
-import (
-	"fmt"
-	"strconv"
-
-	"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
-	"github.com/jesseduffield/lazygit/pkg/commands/models"
-)
-
-func (gui *Gui) createOrOpenPullRequestMenu(selectedBranch *models.Branch, checkedOutBranch *models.Branch) error {
-	menuItems := make([]*menuItem, 0, 4)
-
-	fromToDisplayStrings := func(from string, to string) []string {
-		return []string{fmt.Sprintf("%s → %s", from, to)}
-	}
-
-	menuItemsForBranch := func(branch *models.Branch) []*menuItem {
-		return []*menuItem{
-			{
-				displayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcDefaultBranch),
-				onPress: func() error {
-					return gui.createPullRequest(branch.Name, "")
-				},
-			},
-			{
-				displayStrings: fromToDisplayStrings(branch.Name, gui.Tr.LcSelectBranch),
-				onPress: func() error {
-					return gui.prompt(promptOpts{
-						title:               branch.Name + " →",
-						findSuggestionsFunc: gui.getBranchNameSuggestionsFunc(),
-						handleConfirm: func(targetBranchName string) error {
-							return gui.createPullRequest(branch.Name, targetBranchName)
-						}},
-					)
-				},
-			},
-		}
-	}
-
-	pr, hasPr, err := gui.GetPr(selectedBranch)
-	if err != nil {
-		return err
-	}
-
-	if hasPr {
-		menuItems = append(menuItems, &menuItem{
-			displayString: gui.Git.Gh.Tr.MustSpecifyOriginError + strconv.Itoa(pr.Number),
-			onPress: func() error {
-				return gui.OSCommand.OpenLink(pr.Url)
-			},
-		})
-	}
-
-	if selectedBranch != checkedOutBranch {
-		menuItems = append(menuItems,
-			&menuItem{
-				displayStrings: fromToDisplayStrings(checkedOutBranch.Name, selectedBranch.Name),
-				onPress: func() error {
-					return gui.createPullRequest(checkedOutBranch.Name, selectedBranch.Name)
-				},
-			},
-		)
-		menuItems = append(menuItems, menuItemsForBranch(checkedOutBranch)...)
-	}
-
-	menuItems = append(menuItems, menuItemsForBranch(selectedBranch)...)
-
-	return gui.createMenu(fmt.Sprintf(gui.Tr.CreateOrOpenPullRequestOptions), menuItems, createMenuOptions{showCancel: true})
-}
-
-func (gui *Gui) createPullRequest(from string, to string) error {
-	hostingServiceMgr := gui.getHostingServiceMgr()
-	url, err := hostingServiceMgr.GetPullRequestURL(from, to)
-	if err != nil {
-		return gui.surfaceError(err)
-	}
-
-	// gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf(gui.Tr.CreatingPullRequestAtUrl, url), gui.Tr.CreateOrShowPullRequest, false))
-
-	gui.logAction(gui.Tr.Actions.OpenPullRequest)
-
-	if err := gui.OSCommand.OpenLink(url); err != nil {
-		return gui.surfaceError(err)
-	}
-
-	return nil
-}
-
-func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr {
-	remoteUrl := gui.Git.Config.GetRemoteURL()
-	configServices := gui.UserConfig.Services
-	return hosting_service.NewHostingServiceMgr(gui.Log, gui.Tr, remoteUrl, configServices)
-}
diff --git a/pkg/gui/refresh.go b/pkg/gui/refresh.go
index d09583389..23be7d1ac 100644
--- a/pkg/gui/refresh.go
+++ b/pkg/gui/refresh.go
@@ -237,6 +237,10 @@ func (gui *Gui) refreshCommitsWithLimit() error {
 	}
 	gui.State.Model.Commits = commits
 
+	if gui.Config.GetUserConfig().Git.EnableGhCommand {
+		gui.refreshGithubPullRequests()
+	}
+
 	return gui.c.PostRefreshUpdate(gui.State.Contexts.LocalCommits)
 }
 
diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go
index ab8f6b2b8..ea5164eda 100644
--- a/pkg/gui/types/common.go
+++ b/pkg/gui/types/common.go
@@ -145,6 +145,7 @@ type Model struct {
 	StashEntries []*models.StashEntry
 	SubCommits   []*models.Commit
 	Remotes      []*models.Remote
+	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
diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go
index 66e895359..6b9a94a76 100644
--- a/pkg/i18n/chinese.go
+++ b/pkg/i18n/chinese.go
@@ -163,7 +163,7 @@ func chineseTranslationSet() TranslationSet {
 		SwitchRepo:                          `切换到最近的仓库`,
 		LcAllBranchesLogGraph:               `显示所有分支的日志`,
 		UnsupportedGitService:               `不支持的 git 服务`,
-		LcCreateOrOpenPullRequestOptions:    `创建抓取请求`,
+		LcCreatePullRequest:                 `创建抓取请求`,
 		LcCopyPullRequestURL:                `将抓取请求 URL 复制到剪贴板`,
 		NoBranchOnRemote:                    `该分支在远程上不存在. 您需要先将其推送到远程.`,
 		LcFetch:                             `抓取`,
@@ -436,6 +436,8 @@ func chineseTranslationSet() TranslationSet {
 		LcCopiedToClipboard:                 "复制到剪贴板",
 		ErrCannotEditDirectory:              "无法编辑目录:您只能编辑单个文件",
 		ErrStageDirWithInlineMergeConflicts: "无法 暂存/取消暂存 包含具有内联合并冲突的文件的目录。请先解决合并冲突",
+		SelectRemoteRepository:              "选择存储库",
+		LcSelectingRemote:                   "选择遥控器",
 		ErrRepositoryMovedOrDeleted:         "找不到仓库。它可能已被移动或删除 ¯\\_(ツ)_/¯",
 		CommandLog:                          "命令日志",
 		ToggleShowCommandLog:                "切换 显示/隐藏 命令日志",
diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go
index 1aaee6952..536793fc6 100644
--- a/pkg/i18n/dutch.go
+++ b/pkg/i18n/dutch.go
@@ -128,7 +128,7 @@ func dutchTranslationSet() TranslationSet {
 		SwitchRepo:                          "wissel naar een recente repo",
 		LcAllBranchesLogGraph:               `alle logs van de branch laten zien`,
 		UnsupportedGitService:               `Niet-ondersteunde git-service`,
-		LcCreateOrShowPullRequest:           `maak of laat een pull-request zien`,
+		LcCreatePullRequest:                 `maak een pull-request`,
 		LcCopyPullRequestURL:                `kopieer de URL van het pull-verzoek naar het klembord`,
 		NoBranchOnRemote:                    `Deze branch bestaat niet op de remote. U moet het eerst naar de remote pushen.`,
 		LcFetch:                             `fetch`,
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index d993d70d0..bd9804634 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -162,7 +162,7 @@ type TranslationSet struct {
 	SwitchRepo                          string
 	LcAllBranchesLogGraph               string
 	UnsupportedGitService               string
-	LcCreateOrShowPullRequest           string
+	LcCreatePullRequest                 string
 	LcCopyPullRequestURL                string
 	NoBranchOnRemote                    string
 	LcFetch                             string
@@ -355,6 +355,8 @@ type TranslationSet struct {
 	LcNextScreenMode                    string
 	LcPrevScreenMode                    string
 	LcStartSearch                       string
+	SelectRemoteRepository              string
+	LcSelectingRemote                   string
 	Panel                               string
 	Keybindings                         string
 	LcRenameBranch                      string
@@ -472,16 +474,12 @@ type TranslationSet struct {
 	ToggleWhitespaceInDiffView          string
 	IgnoringWhitespaceInDiffView        string
 	ShowingWhitespaceInDiffView         string
-	CreateOrOpenPullRequestOptions      string
-	LcCreateOrOpenPullRequestOptions    string
-	LcDefaultBranch                     string
-	LcSelectBranch                      string
-	CreateOrShowPullRequest             string
-	CreatingPullRequestAtUrl            string
 	IncreaseContextInDiffView           string
 	DecreaseContextInDiffView           string
 	CreatePullRequestOptions            string
 	LcCreatePullRequestOptions          string
+	LcDefaultBranch                     string
+	LcSelectBranch                      string
 	CreatePullRequest                   string
 	SelectConfigFile                    string
 	NoConfigFileFoundErr                string
@@ -490,11 +488,8 @@ type TranslationSet struct {
 	MustSpecifyOriginError              string
 	GitOutput                           string
 	GitCommandFailed                    string
-	OpenPr                              string
 	AbortTitle                          string
 	AbortPrompt                         string
-	SelectRemoteRepository              string
-	LcSelectingRemote                   string
 	LcOpenLogMenu                       string
 	LogMenuTitle                        string
 	ToggleShowGitGraphAll               string
@@ -808,7 +803,7 @@ func EnglishTranslationSet() TranslationSet {
 		SwitchRepo:                          `switch to a recent repo`,
 		LcAllBranchesLogGraph:               `show all branch logs`,
 		UnsupportedGitService:               `Unsupported git service`,
-		LcCreateOrShowPullRequest:           `create / open pull request`,
+		LcCreatePullRequest:                 `create pull request`,
 		LcCopyPullRequestURL:                `copy pull request URL to clipboard`,
 		NoBranchOnRemote:                    `This branch doesn't exist on remote. You need to push it to remote first.`,
 		LcFetch:                             `fetch`,
@@ -1002,6 +997,8 @@ func EnglishTranslationSet() TranslationSet {
 		LcNextScreenMode:                    "next screen mode (normal/half/fullscreen)",
 		LcPrevScreenMode:                    "prev screen mode",
 		LcStartSearch:                       "start search",
+		LcSelectingRemote:                   "selecting remote",
+		SelectRemoteRepository:              "select base remote repository",
 		Panel:                               "Panel",
 		Keybindings:                         "Keybindings",
 		LcRenameBranch:                      "rename branch",
@@ -1100,8 +1097,6 @@ func EnglishTranslationSet() TranslationSet {
 		SuggestionsTitle:                    "Suggestions (press %s to focus)",
 		ExtrasTitle:                         "Command Log",
 		PushingTagStatus:                    "pushing tag",
-		SelectRemoteRepository:              "select base remote repository",
-		LcSelectingRemote:                   "selecting remote",
 		PullRequestURLCopiedToClipboard:     "Pull request URL copied to clipboard",
 		CommitDiffCopiedToClipboard:         "Commit diff copied to clipboard",
 		CommitSHACopiedToClipboard:          "Commit SHA copied to clipboard",
@@ -1121,9 +1116,6 @@ func EnglishTranslationSet() TranslationSet {
 		ToggleWhitespaceInDiffView:          "Toggle whether or not whitespace changes are shown in the diff view",
 		IgnoringWhitespaceInDiffView:        "Whitespace will be ignored in the diff view",
 		ShowingWhitespaceInDiffView:         "Whitespace will be shown in the diff view",
-		CreateOrShowPullRequest:             "create / open pull request",
-		CreateOrOpenPullRequestOptions:      "create / open pull request options",
-		LcCreateOrOpenPullRequestOptions:    "create / open pull request options",
 		IncreaseContextInDiffView:           "Increase the size of the context shown around changes in the diff view",
 		DecreaseContextInDiffView:           "Decrease the size of the context shown around changes in the diff view",
 		CreatePullRequest:                   "Create pull request",
@@ -1138,7 +1130,6 @@ func EnglishTranslationSet() TranslationSet {
 		MustSpecifyOriginError:              "Must specify a remote if specifying a branch",
 		GitOutput:                           "Git output:",
 		GitCommandFailed:                    "Git command failed. Check command log for details (open with %s)",
-		OpenPr:                              "Open PR #",
 		AbortTitle:                          "Abort %s",
 		AbortPrompt:                         "Are you sure you want to abort the current %s?",
 		LcOpenLogMenu:                       "open log menu",
diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go
index 74ea00673..e82ddc2de 100644
--- a/pkg/i18n/polish.go
+++ b/pkg/i18n/polish.go
@@ -102,7 +102,7 @@ func polishTranslationSet() TranslationSet {
 		ConfirmQuit:                         "Na pewno chcesz wyjść z programu?",
 		LcAllBranchesLogGraph:               "pokaż wszystkie logi gałęzi",
 		UnsupportedGitService:               "Nieobsługiwana usługa git",
-		LcCreateOrShowPullRequest:           `maak of laat een pull-request zien`,
+		LcCreatePullRequest:                 "utwórz żądanie pobrania",
 		LcCopyPullRequestURL:                "skopiuj adres URL żądania pobrania do schowka",
 		NoBranchOnRemote:                    "Ta gałąź nie istnieje w zdalnym repo. Najpierw musisz ją wysłać.",
 		LcFetch:                             "pobierz",
@@ -225,8 +225,6 @@ func polishTranslationSet() TranslationSet {
 		PullRequestURLCopiedToClipboard:     "URL żądania ściągnięcia skopiowany do schowka",
 		CommitMessageCopiedToClipboard:      "Komunikat commita skopiowany do schowka",
 		LcCopiedToClipboard:                 "skopiowany do schowka",
-		CreateOrOpenPullRequestOptions:      "Utwórz opcje żądania ściągnięcia",
-		LcCreateOrOpenPullRequestOptions:    "utwórz opcje żądania",
 		CreatePullRequestOptions:            "Utwórz opcje żądania ściągnięcia",
 		LcCreatePullRequestOptions:          "utwórz opcje żądania ściągnięcia",
 		ConfirmRevertCommit:                 "Czy na pewno chcesz obrócić {{.selectedCommit}}?",