From c295deaa81137e846389cfb9ca1a0d7bfcc671be Mon Sep 17 00:00:00 2001 From: mjarkk Date: Wed, 28 Jul 2021 22:01:35 +0200 Subject: [PATCH] show github pr number in branches list --- docs/keybindings/Keybindings_en.md | 4 +- docs/keybindings/Keybindings_nl.md | 2 +- pkg/commands/git.go | 3 ++ pkg/commands/github.go | 65 ++++++++++++++++++++++++++ pkg/commands/github_test.go | 75 ++++++++++++++++++++++++++++++ pkg/commands/loading_branches.go | 16 +++++-- pkg/commands/models/branch.go | 1 + pkg/commands/models/github.go | 15 ++++++ pkg/commands/pull_request.go | 27 +---------- pkg/commands/pull_request_test.go | 2 +- pkg/commands/remotes.go | 64 +++++++++++++++++++++++++ pkg/config/user_config.go | 52 ++++++++++----------- pkg/gui/branches_panel.go | 18 +++++-- pkg/gui/commits_panel.go | 13 +++++- pkg/gui/gui.go | 13 +++--- pkg/gui/keybindings.go | 10 ++-- pkg/gui/list_context_config.go | 7 ++- pkg/gui/presentation/branches.go | 28 ++++++++--- pkg/gui/pull_request_menu_panel.go | 16 +++++-- pkg/i18n/chinese.go | 2 +- pkg/i18n/dutch.go | 6 +-- pkg/i18n/english.go | 16 +++---- pkg/i18n/polish.go | 6 +-- 23 files changed, 360 insertions(+), 101 deletions(-) create mode 100644 pkg/commands/github.go create mode 100644 pkg/commands/github_test.go create mode 100644 pkg/commands/models/github.go diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index 1cc81936d..3845d860e 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -39,8 +39,8 @@
   space: checkout
-  o: create pull request
-  O: create pull request options
+  o: create / show pull request
+  O: create / show pull request options
   ctrl+y: copy pull request URL to clipboard
   c: checkout by name
   F: force checkout
diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md
index 56158c6fb..0f547524f 100644
--- a/docs/keybindings/Keybindings_nl.md
+++ b/docs/keybindings/Keybindings_nl.md
@@ -39,7 +39,7 @@
 
 
   space: uitchecken
-  o: maak een pull-request
+  o: maak of laat een pull-request zien
   O: bekijk opties voor pull-aanvraag
   ctrl+y: kopieer de URL van het pull-verzoek naar het klembord
   c: uitchecken bij naam
diff --git a/pkg/commands/git.go b/pkg/commands/git.go
index 3a9e434cb..b24bd5d0e 100644
--- a/pkg/commands/git.go
+++ b/pkg/commands/git.go
@@ -10,6 +10,7 @@ import (
 	"github.com/go-errors/errors"
 
 	gogit "github.com/jesseduffield/go-git/v5"
+	"github.com/jesseduffield/lazygit/pkg/commands/models"
 	"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
 	"github.com/jesseduffield/lazygit/pkg/commands/patch"
 	"github.com/jesseduffield/lazygit/pkg/config"
@@ -39,6 +40,8 @@ type GitCommand struct {
 
 	// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
 	PushToCurrent bool
+
+	GithubRecentPRs map[string]models.GithubPullRequest
 }
 
 // NewGitCommand it runs git commands
diff --git a/pkg/commands/github.go b/pkg/commands/github.go
new file mode 100644
index 000000000..c78108e73
--- /dev/null
+++ b/pkg/commands/github.go
@@ -0,0 +1,65 @@
+package commands
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/jesseduffield/lazygit/pkg/commands/models"
+)
+
+func (c *GitCommand) GithubMostRecentPRs() map[string]models.GithubPullRequest {
+	commandOutput, err := c.OSCommand.RunCommandWithOutput("gh pr list --limit 50 --state all --json state,url,number,headRefName,headRepositoryOwner")
+	if err != nil {
+		fmt.Println(1, err)
+		return nil
+	}
+
+	prs := []models.GithubPullRequest{}
+	err = json.Unmarshal([]byte(commandOutput), &prs)
+	if err != nil {
+		fmt.Println(2, err)
+		return nil
+	}
+
+	res := map[string]models.GithubPullRequest{}
+	for _, pr := range prs {
+		res[pr.HeadRepositoryOwner.Login+":"+pr.HeadRefName] = pr
+	}
+	return res
+}
+
+func (c *GitCommand) InjectGithubPullRequests(prs map[string]models.GithubPullRequest, branches []*models.Branch) bool {
+	if len(prs) == 0 {
+		return false
+	}
+
+	remotesToOwnersMap, _ := c.GetRemotesToOwnersMap()
+	if len(remotesToOwnersMap) == 0 {
+		return false
+	}
+
+	foundBranchWithGithubPullRequest := false
+
+	for _, branch := range branches {
+		if branch.UpstreamName == "" {
+			continue
+		}
+
+		remoteAndName := strings.SplitN(branch.UpstreamName, "/", 2)
+		owner, foundRemoteOwner := remotesToOwnersMap[remoteAndName[0]]
+		if len(remoteAndName) != 2 || !foundRemoteOwner {
+			continue
+		}
+
+		pr, hasPr := prs[owner+":"+remoteAndName[1]]
+		if !hasPr {
+			continue
+		}
+
+		foundBranchWithGithubPullRequest = true
+		branch.PR = &pr
+	}
+
+	return foundBranchWithGithubPullRequest
+}
diff --git a/pkg/commands/github_test.go b/pkg/commands/github_test.go
new file mode 100644
index 000000000..befd4ec30
--- /dev/null
+++ b/pkg/commands/github_test.go
@@ -0,0 +1,75 @@
+package commands
+
+import (
+	"os/exec"
+	"testing"
+
+	"github.com/jesseduffield/lazygit/pkg/commands/models"
+	"github.com/jesseduffield/lazygit/pkg/secureexec"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGithubMostRecentPRs(t *testing.T) {
+	scenarios := []struct {
+		testName string
+		response string
+		expect   map[string]models.GithubPullRequest
+	}{
+		{
+			"no response",
+			"",
+			nil,
+		},
+		{
+			"error response",
+			"none of the git remotes configured for this repository point to a known GitHub host. To tell gh about a new GitHub host, please use `gh auth login`",
+			nil,
+		},
+		{
+			"empty response",
+			"[]",
+			map[string]models.GithubPullRequest{},
+		},
+		{
+			"response with data",
+			`[{
+				"headRefName": "command-log-2",
+				"number": 1249,
+				"state": "MERGED",
+				"url": "https://github.com/jesseduffield/lazygit/pull/1249",
+				"headRepositoryOwner": {
+					"id": "MDQ6VXNlcjg0NTY2MzM=",
+					"name": "Jesse Duffield",
+					"login": "jesseduffield"
+				}
+			}]`,
+			map[string]models.GithubPullRequest{
+				"jesseduffield:command-log-2": {
+					HeadRefName: "command-log-2",
+					Number:      1249,
+					State:       "MERGED",
+					Url:         "https://github.com/jesseduffield/lazygit/pull/1249",
+					HeadRepositoryOwner: models.GithubRepositoryOwner{
+						ID:    "MDQ6VXNlcjg0NTY2MzM=",
+						Name:  "Jesse Duffield",
+						Login: "jesseduffield",
+					},
+				},
+			},
+		},
+	}
+
+	for _, s := range scenarios {
+		t.Run(s.testName, func(t *testing.T) {
+			gitCmd := NewDummyGitCommand()
+			gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
+				assert.EqualValues(t, "gh", cmd)
+				assert.EqualValues(t, []string{"pr", "list", "--limit", "50", "--state", "all", "--json", "state,url,number,headRefName,headRepositoryOwner"}, args)
+				return secureexec.Command("echo", s.response)
+			}
+
+			res := gitCmd.GithubMostRecentPRs()
+			assert.Equal(t, s.expect, res)
+		})
+	}
+}
diff --git a/pkg/commands/loading_branches.go b/pkg/commands/loading_branches.go
index 7565a9aee..eaee8e5fb 100644
--- a/pkg/commands/loading_branches.go
+++ b/pkg/commands/loading_branches.go
@@ -99,8 +99,8 @@ func (b *BranchListBuilder) obtainBranches() []*models.Branch {
 }
 
 // Build the list of branches for the current repo
-func (b *BranchListBuilder) Build() []*models.Branch {
-	branches := b.obtainBranches()
+func (b *BranchListBuilder) Build() (branches []*models.Branch, branchesWithGithubPullRequests bool) {
+	branches = b.obtainBranches()
 
 	reflogBranches := b.obtainReflogBranches()
 
@@ -138,9 +138,17 @@ outer:
 		if err != nil {
 			panic(err)
 		}
-		branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: "  *"}}, branches...)
+		branches = append([]*models.Branch{{
+			Name:        currentBranchName,
+			DisplayName: currentBranchDisplayName,
+			Head:        true,
+			Recency:     "  *",
+		}}, branches...)
 	}
-	return branches
+
+	branchesWithGithubPullRequests = b.GitCommand.InjectGithubPullRequests(b.GitCommand.GithubRecentPRs, branches)
+
+	return
 }
 
 // TODO: only look at the new reflog commits, and otherwise store the recencies in
diff --git a/pkg/commands/models/branch.go b/pkg/commands/models/branch.go
index 3b8268bff..9199a3611 100644
--- a/pkg/commands/models/branch.go
+++ b/pkg/commands/models/branch.go
@@ -11,6 +11,7 @@ type Branch struct {
 	Pullables    string
 	UpstreamName string
 	Head         bool
+	PR           *GithubPullRequest
 }
 
 func (b *Branch) RefName() string {
diff --git a/pkg/commands/models/github.go b/pkg/commands/models/github.go
new file mode 100644
index 000000000..088a066c4
--- /dev/null
+++ b/pkg/commands/models/github.go
@@ -0,0 +1,15 @@
+package models
+
+type GithubPullRequest struct {
+	HeadRefName         string                `json:"headRefName"`
+	Number              int                   `json:"number"`
+	State               string                `json:"state"` // "MERGED", "OPEN", "CLOSED"
+	Url                 string                `json:"url"`
+	HeadRepositoryOwner GithubRepositoryOwner `json:"headRepositoryOwner"`
+}
+
+type GithubRepositoryOwner struct {
+	ID    string `json:"id"`
+	Name  string `json:"name"`
+	Login string `json:"login"`
+}
diff --git a/pkg/commands/pull_request.go b/pkg/commands/pull_request.go
index 46dcbfd68..0d2715c0b 100644
--- a/pkg/commands/pull_request.go
+++ b/pkg/commands/pull_request.go
@@ -153,34 +153,9 @@ func (pr *PullRequest) getPullRequestURL(from string, to string) (string, error)
 		return "", errors.New(pr.GitCommand.Tr.UnsupportedGitService)
 	}
 
-	repoInfo := getRepoInfoFromURL(repoURL)
+	repoInfo := GetRepoInfoFromURL(repoURL)
 
 	pullRequestURL := gitService.PullRequestURL(repoInfo.Owner, repoInfo.Repository, from, to)
 
 	return pullRequestURL, nil
 }
-
-func getRepoInfoFromURL(url string) *RepoInformation {
-	isHTTP := strings.HasPrefix(url, "http")
-
-	if isHTTP {
-		splits := strings.Split(url, "/")
-		owner := strings.Join(splits[3:len(splits)-1], "/")
-		repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
-
-		return &RepoInformation{
-			Owner:      owner,
-			Repository: repo,
-		}
-	}
-
-	tmpSplit := strings.Split(url, ":")
-	splits := strings.Split(tmpSplit[1], "/")
-	owner := strings.Join(splits[0:len(splits)-1], "/")
-	repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
-
-	return &RepoInformation{
-		Owner:      owner,
-		Repository: repo,
-	}
-}
diff --git a/pkg/commands/pull_request_test.go b/pkg/commands/pull_request_test.go
index 2db5b8ade..2af57529f 100644
--- a/pkg/commands/pull_request_test.go
+++ b/pkg/commands/pull_request_test.go
@@ -38,7 +38,7 @@ func TestGetRepoInfoFromURL(t *testing.T) {
 
 	for _, s := range scenarios {
 		t.Run(s.testName, func(t *testing.T) {
-			s.test(getRepoInfoFromURL(s.repoURL))
+			s.test(GetRepoInfoFromURL(s.repoURL))
 		})
 	}
 }
diff --git a/pkg/commands/remotes.go b/pkg/commands/remotes.go
index 75dee0b46..1eb24d761 100644
--- a/pkg/commands/remotes.go
+++ b/pkg/commands/remotes.go
@@ -2,6 +2,7 @@ package commands
 
 import (
 	"fmt"
+	"strings"
 )
 
 func (c *GitCommand) AddRemote(name string, url string) error {
@@ -39,3 +40,66 @@ func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
 func (c *GitCommand) GetRemoteURL() string {
 	return c.GetConfigValue("remote.origin.url")
 }
+
+func (c *GitCommand) GetRemoteURLs() (map[string]string, error) {
+	res := map[string]string{}
+	out, err := c.OSCommand.RunCommandWithOutput("git remote -v")
+	if err != nil {
+		return nil, err
+	}
+	lines := strings.Split(strings.TrimSpace(out), "\n")
+	for _, line := range lines {
+		lineParts := strings.Split(line, "\t")
+		if len(lineParts) < 2 {
+			continue
+		}
+
+		name := lineParts[0] // "origin"
+		for _, mightBeUrl := range lineParts[1:] {
+			if len(mightBeUrl) > 0 {
+				// mightBeUrl = "git@github.com:jesseduffield/lazygit.git (fetch)"
+				res[name] = strings.SplitN(mightBeUrl, " ", 2)[0]
+				break
+			}
+		}
+	}
+	return res, nil
+}
+
+func GetRepoInfoFromURL(url string) *RepoInformation {
+	isHTTP := strings.HasPrefix(url, "http")
+
+	if isHTTP {
+		splits := strings.Split(url, "/")
+		owner := strings.Join(splits[3:len(splits)-1], "/")
+		repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
+
+		return &RepoInformation{
+			Owner:      owner,
+			Repository: repo,
+		}
+	}
+
+	tmpSplit := strings.Split(url, ":")
+	splits := strings.Split(tmpSplit[1], "/")
+	owner := strings.Join(splits[0:len(splits)-1], "/")
+	repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
+
+	return &RepoInformation{
+		Owner:      owner,
+		Repository: repo,
+	}
+}
+
+func (c *GitCommand) GetRemotesToOwnersMap() (map[string]string, error) {
+	remotes, err := c.GetRemoteURLs()
+	if err != nil {
+		return nil, err
+	}
+
+	res := map[string]string{}
+	for remoteName, remoteUrl := range remotes {
+		res[remoteName] = GetRepoInfoFromURL(remoteUrl).Owner
+	}
+	return res, nil
+}
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index 38259f6f2..9ad7578a7 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -192,19 +192,19 @@ type KeybindingFilesConfig struct {
 }
 
 type KeybindingBranchesConfig struct {
-	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"`
+	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"`
 }
 
 type KeybindingCommitsConfig struct {
@@ -436,19 +436,19 @@ func GetDefaultConfig() *UserConfig {
 				OpenMergeTool:            "M",
 			},
 			Branches: KeybindingBranchesConfig{
-				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",
+				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",
 			},
 			Commits: KeybindingCommitsConfig{
 				SquashDown:                   "s",
diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go
index 9e44a8a8c..18227992a 100644
--- a/pkg/gui/branches_panel.go
+++ b/pkg/gui/branches_panel.go
@@ -68,7 +68,7 @@ func (gui *Gui) refreshBranches() {
 	if err != nil {
 		_ = gui.surfaceError(err)
 	}
-	gui.State.Branches = builder.Build()
+	gui.State.Branches, gui.State.BranchesWithGithubPullRequests = builder.Build()
 
 	if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil {
 		gui.Log.Error(err)
@@ -77,6 +77,13 @@ func (gui *Gui) refreshBranches() {
 	gui.refreshStatus()
 }
 
+func (gui *Gui) refreshGithubPullRequests() {
+	prs := gui.GitCommand.GithubMostRecentPRs()
+	if len(prs) > 0 {
+		gui.GitCommand.GithubRecentPRs = prs
+	}
+}
+
 // specific functions
 
 func (gui *Gui) handleBranchPress() error {
@@ -90,19 +97,22 @@ func (gui *Gui) handleBranchPress() error {
 	return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutBranch})
 }
 
-func (gui *Gui) handleCreatePullRequestPress() error {
+func (gui *Gui) handleCreateOrShowPullRequestPress() error {
 	branch := gui.getSelectedBranch()
+	if branch.PR != nil {
+		return gui.OSCommand.OpenLink(branch.PR.Url)
+	}
 	return gui.createPullRequest(branch.Name, "")
 }
 
-func (gui *Gui) handleCreatePullRequestMenu() error {
+func (gui *Gui) handleCreateOrOpenPullRequestMenu() error {
 	selectedBranch := gui.getSelectedBranch()
 	if selectedBranch == nil {
 		return nil
 	}
 	checkedOutBranch := gui.getCheckedOutBranch()
 
-	return gui.createPullRequestMenu(selectedBranch, checkedOutBranch)
+	return gui.createOrOpenPullRequestMenu(selectedBranch, checkedOutBranch)
 }
 
 func (gui *Gui) handleCopyPullRequestURLPress() error {
diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go
index 422d8ec29..04b0c8a61 100644
--- a/pkg/gui/commits_panel.go
+++ b/pkg/gui/commits_panel.go
@@ -61,12 +61,23 @@ func (gui *Gui) handleCommitSelect() error {
 func (gui *Gui) refreshReflogCommitsConsideringStartup() {
 	switch gui.State.StartupStage {
 	case INITIAL:
+		var wg sync.WaitGroup
+		wg.Add(1)
+
 		go utils.Safe(func() {
 			_ = gui.refreshReflogCommits()
 			gui.refreshBranches()
 			gui.State.StartupStage = COMPLETE
+			wg.Done()
+		})
+		go utils.Safe(func() {
+			// The github cli can be quite slow so we load the github PRs sparately
+			gui.refreshGithubPullRequests()
+			wg.Wait()
+			gui.State.BranchesWithGithubPullRequests = gui.GitCommand.InjectGithubPullRequests(gui.GitCommand.GithubRecentPRs, gui.State.Branches)
+			_ = gui.postRefreshUpdate(gui.State.Contexts.Branches)
+			gui.refreshStatus()
 		})
-
 	case COMPLETE:
 		_ = gui.refreshReflogCommits()
 	}
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 71b9702f2..89dc71374 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -289,12 +289,13 @@ type guiMutexes struct {
 type guiState struct {
 	// the file panels (files and commit files) can render as a tree, so we have
 	// managers for them which handle rendering a flat list of files in tree form
-	FileManager       *filetree.FileManager
-	CommitFileManager *filetree.CommitFileManager
-	Submodules        []*models.SubmoduleConfig
-	Branches          []*models.Branch
-	Commits           []*models.Commit
-	StashEntries      []*models.StashEntry
+	FileManager                    *filetree.FileManager
+	CommitFileManager              *filetree.CommitFileManager
+	Submodules                     []*models.SubmoduleConfig
+	Branches                       []*models.Branch
+	BranchesWithGithubPullRequests bool
+	Commits                        []*models.Commit
+	StashEntries                   []*models.StashEntry
 	// Suggestions will sometimes appear when typing into a prompt
 	Suggestions []*types.Suggestion
 	// FilteredReflogCommits are the ones that appear in the reflog panel.
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index f9537e755..695f0b052 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -541,16 +541,16 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
 		{
 			ViewName:    "branches",
 			Contexts:    []string{string(LOCAL_BRANCHES_CONTEXT_KEY)},
-			Key:         gui.getKey(config.Branches.CreatePullRequest),
-			Handler:     gui.handleCreatePullRequestPress,
-			Description: gui.Tr.LcCreatePullRequest,
+			Key:         gui.getKey(config.Branches.CreateOrShowPullRequest),
+			Handler:     gui.handleCreateOrShowPullRequestPress,
+			Description: gui.Tr.LcCreateOrShowPullRequest,
 		},
 		{
 			ViewName:    "branches",
 			Contexts:    []string{string(LOCAL_BRANCHES_CONTEXT_KEY)},
 			Key:         gui.getKey(config.Branches.ViewPullRequestOptions),
-			Handler:     gui.handleCreatePullRequestMenu,
-			Description: gui.Tr.LcCreatePullRequestOptions,
+			Handler:     gui.handleCreateOrOpenPullRequestMenu,
+			Description: gui.Tr.LcCreateOrOpenPullRequestOptions,
 			OpensMenu:   true,
 		},
 		{
diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go
index 6e9563069..01fd1d117 100644
--- a/pkg/gui/list_context_config.go
+++ b/pkg/gui/list_context_config.go
@@ -70,7 +70,12 @@ func (gui *Gui) branchesListContext() *ListContext {
 		Gui:                        gui,
 		ResetMainViewOriginOnFocus: true,
 		GetDisplayStrings: func() [][]string {
-			return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
+			return presentation.GetBranchListDisplayStrings(
+				gui.State.Branches,
+				gui.State.ScreenMode != SCREEN_NORMAL,
+				gui.State.Modes.Diffing.Ref,
+				gui.State.BranchesWithGithubPullRequests,
+			)
 		},
 		SelectedItem: func() (ListItem, bool) {
 			item := gui.getSelectedBranch()
diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go
index 3e51e3d66..083f26e43 100644
--- a/pkg/gui/presentation/branches.go
+++ b/pkg/gui/presentation/branches.go
@@ -2,6 +2,7 @@ package presentation
 
 import (
 	"fmt"
+	"strconv"
 	"strings"
 
 	"github.com/fatih/color"
@@ -10,19 +11,19 @@ import (
 	"github.com/jesseduffield/lazygit/pkg/utils"
 )
 
-func GetBranchListDisplayStrings(branches []*models.Branch, fullDescription bool, diffName string) [][]string {
+func GetBranchListDisplayStrings(branches []*models.Branch, fullDescription bool, diffName string, showGithub bool) [][]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, showGithub)
 	}
 
 	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, showGithub bool) []string {
 	displayName := b.Name
 	if b.DisplayName != "" {
 		displayName = b.DisplayName
@@ -42,11 +43,26 @@ func getBranchDisplayStrings(b *models.Branch, fullDescription bool, diffed bool
 		recencyColor = color.FgGreen
 	}
 
-	if fullDescription {
-		return []string{utils.ColoredString(b.Recency, recencyColor), coloredName, utils.ColoredString(b.UpstreamName, color.FgYellow)}
+	res := []string{utils.ColoredString(b.Recency, recencyColor)}
+	if showGithub {
+		if b.PR != nil {
+			colour := color.FgMagenta // = state MERGED
+			switch b.PR.State {
+			case "OPEN":
+				colour = color.FgGreen
+			case "CLOSED":
+				colour = color.FgRed
+			}
+			res = append(res, utils.ColoredString("#"+strconv.Itoa(b.PR.Number), colour))
+		} else {
+			res = append(res, "")
+		}
 	}
 
-	return []string{utils.ColoredString(b.Recency, recencyColor), coloredName}
+	if fullDescription {
+		return append(res, coloredName, utils.ColoredString(b.UpstreamName, color.FgYellow))
+	}
+	return append(res, coloredName)
 }
 
 // GetBranchColor branch color
diff --git a/pkg/gui/pull_request_menu_panel.go b/pkg/gui/pull_request_menu_panel.go
index aa7bc481a..83665e197 100644
--- a/pkg/gui/pull_request_menu_panel.go
+++ b/pkg/gui/pull_request_menu_panel.go
@@ -2,13 +2,14 @@ package gui
 
 import (
 	"fmt"
+	"strconv"
 
 	"github.com/jesseduffield/lazygit/pkg/commands"
 	"github.com/jesseduffield/lazygit/pkg/commands/models"
 	"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
 )
 
-func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutBranch *models.Branch) error {
+func (gui *Gui) createOrOpenPullRequestMenu(selectedBranch *models.Branch, checkedOutBranch *models.Branch) error {
 	menuItems := make([]*menuItem, 0, 4)
 
 	fromToDisplayStrings := func(from string, to string) []string {
@@ -38,6 +39,15 @@ func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutB
 		}
 	}
 
+	if selectedBranch.PR != nil {
+		menuItems = append(menuItems, &menuItem{
+			displayString: "open #" + strconv.Itoa(selectedBranch.PR.Number),
+			onPress: func() error {
+				return gui.OSCommand.OpenLink(selectedBranch.PR.Url)
+			},
+		})
+	}
+
 	if selectedBranch != checkedOutBranch {
 		menuItems = append(menuItems,
 			&menuItem{
@@ -52,7 +62,7 @@ func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutB
 
 	menuItems = append(menuItems, menuItemsForBranch(selectedBranch)...)
 
-	return gui.createMenu(fmt.Sprintf(gui.Tr.CreatePullRequestOptions), menuItems, createMenuOptions{showCancel: true})
+	return gui.createMenu(fmt.Sprintf(gui.Tr.CreateOrOpenPullRequestOptions), menuItems, createMenuOptions{showCancel: true})
 }
 
 func (gui *Gui) createPullRequest(from string, to string) error {
@@ -61,7 +71,7 @@ func (gui *Gui) createPullRequest(from string, to string) error {
 	if err != nil {
 		return gui.surfaceError(err)
 	}
-	gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf(gui.Tr.CreatingPullRequestAtUrl, url), gui.Tr.CreatePullRequest, false))
+	gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf(gui.Tr.CreatingPullRequestAtUrl, url), gui.Tr.CreateOrShowPullRequest, false))
 
 	return nil
 }
diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go
index 94fcda8c4..30d322c42 100644
--- a/pkg/i18n/chinese.go
+++ b/pkg/i18n/chinese.go
@@ -176,7 +176,7 @@ func chineseTranslationSet() TranslationSet {
 		SwitchRepo:                          `切换到最近的仓库`,
 		LcAllBranchesLogGraph:               `显示所有分支日志`,
 		UnsupportedGitService:               `不支持的git服务`,
-		LcCreatePullRequest:                 `创建pull请求`,
+		LcCreateOrShowPullRequest:           `创建pull请求`,
 		LcCopyPullRequestURL:                `将拉取请求URL复制到剪贴板`,
 		NoBranchOnRemote:                    `该分支在远程上不存在。您需要先将其推送到远程.`,
 		LcFetch:                             `fetch`,
diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go
index cd8e2160c..0b56ffe78 100644
--- a/pkg/i18n/dutch.go
+++ b/pkg/i18n/dutch.go
@@ -154,7 +154,7 @@ func dutchTranslationSet() TranslationSet {
 		SwitchRepo:                          "wissel naar een recente repo",
 		LcAllBranchesLogGraph:               `alle logs van de branch laten zien`,
 		UnsupportedGitService:               `Niet-ondersteunde git-service`,
-		LcCreatePullRequest:                 `maak een pull-request`,
+		LcCreateOrShowPullRequest:           `maak of laat een pull-request zien`,
 		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`,
@@ -396,7 +396,7 @@ func dutchTranslationSet() TranslationSet {
 		LcInitSubmodule:                     "initialiseer submodule",
 		LcViewBulkSubmoduleOptions:          "bekijk bulk submodule opties",
 		LcViewStashFiles:                    "bekijk bestanden van stash entry",
-		CreatePullRequestOptions:            "Bekijk opties voor pull-aanvraag",
-		LcCreatePullRequestOptions:          "bekijk opties voor pull-aanvraag",
+		CreateOrOpenPullRequestOptions:      "Bekijk opties voor pull-aanvraag",
+		LcCreateOrOpenPullRequestOptions:    "bekijk opties voor pull-aanvraag",
 	}
 }
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index e2a9f38de..bfb0b7d54 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -169,7 +169,7 @@ type TranslationSet struct {
 	SwitchRepo                          string
 	LcAllBranchesLogGraph               string
 	UnsupportedGitService               string
-	LcCreatePullRequest                 string
+	LcCreateOrShowPullRequest           string
 	LcCopyPullRequestURL                string
 	NoBranchOnRemote                    string
 	LcFetch                             string
@@ -454,11 +454,11 @@ type TranslationSet struct {
 	ToggleWhitespaceInDiffView          string
 	IgnoringWhitespaceInDiffView        string
 	ShowingWhitespaceInDiffView         string
-	CreatePullRequestOptions            string
-	LcCreatePullRequestOptions          string
+	CreateOrOpenPullRequestOptions      string
+	LcCreateOrOpenPullRequestOptions    string
 	LcDefaultBranch                     string
 	LcSelectBranch                      string
-	CreatePullRequest                   string
+	CreateOrShowPullRequest             string
 	CreatingPullRequestAtUrl            string
 	Spans                               Spans
 }
@@ -720,7 +720,7 @@ func englishTranslationSet() TranslationSet {
 		SwitchRepo:                          `switch to a recent repo`,
 		LcAllBranchesLogGraph:               `show all branch logs`,
 		UnsupportedGitService:               `Unsupported git service`,
-		LcCreatePullRequest:                 `create pull request`,
+		LcCreateOrShowPullRequest:           `create / show 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`,
@@ -1007,9 +1007,9 @@ 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",
-		CreatePullRequest:                   "Create pull request",
-		CreatePullRequestOptions:            "Create pull request options",
-		LcCreatePullRequestOptions:          "create pull request options",
+		CreateOrShowPullRequest:             "Create / show pull request",
+		CreateOrOpenPullRequestOptions:      "Create / show pull request options",
+		LcCreateOrOpenPullRequestOptions:    "create / show pull request options",
 		LcDefaultBranch:                     "default branch",
 		LcSelectBranch:                      "select branch",
 		CreatingPullRequestAtUrl:            "Creating pull request at URL: %s",
diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go
index 1420d2ef9..aed1ceb10 100644
--- a/pkg/i18n/polish.go
+++ b/pkg/i18n/polish.go
@@ -127,7 +127,7 @@ func polishTranslationSet() TranslationSet {
 		ConfirmQuit:                         `Na pewno chcesz wyjść z programu?`,
 		LcAllBranchesLogGraph:               `pokazywać wszystkie logi branżowe`,
 		UnsupportedGitService:               `Nieobsługiwana usługa git`,
-		LcCreatePullRequest:                 `utwórz żądanie wyciągnięcia`,
+		LcCreateOrShowPullRequest:           `utwórz żądanie wyciągnięcia`,
 		LcCopyPullRequestURL:                `skopiuj adres URL żądania ściągnięcia do schowka`,
 		NoBranchOnRemote:                    `Ta gałąź nie istnieje na zdalnym. Najpierw musisz go odepchnąć na odległość.`,
 		LcFetch:                             `fetch`,
@@ -255,7 +255,7 @@ func polishTranslationSet() TranslationSet {
 		PullRequestURLCopiedToClipboard:     "URL żądania ściągnięcia skopiowany do schowka",
 		CommitMessageCopiedToClipboard:      "Commit message skopiowany do schowka",
 		LcCopiedToClipboard:                 "skopiowany do schowka",
-		CreatePullRequestOptions:            "Utwórz opcje żądania ściągnięcia",
-		LcCreatePullRequestOptions:          "utwórz opcje żądania ściągnięcia",
+		CreateOrOpenPullRequestOptions:      "Utwórz opcje żądania ściągnięcia",
+		LcCreateOrOpenPullRequestOptions:    "utwórz opcje żądania ściągnięcia",
 	}
 }