diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index 95e713826..e97e1dd88 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -43,8 +43,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
   space: przełącz
-  o: utwórz żądanie pobrania
-  O: utwórz opcje żądania ściągnięcia
+  o: maak of laat een pull-request zien
+  O: utwórz opcje żądania
   ctrl+y: skopiuj adres URL żądania pobrania do schowka
   c: przełącz używając nazwy
   F: wymuś przełączenie
diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md
index 42a4fd1c7..6c8a35c5b 100644
--- a/docs/keybindings/Keybindings_zh.md
+++ b/docs/keybindings/Keybindings_zh.md
@@ -44,7 +44,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
 
   space: 检出
   o: 创建抓取请求
-  O: 创建抓取请求选项
+  O: 创建抓取请求
   ctrl+y: 将抓取请求 URL 复制到剪贴板
   c: 按名称检出
   F: 强制检出
diff --git a/pkg/app/app.go b/pkg/app/app.go
index cb3220cee..680a62049 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -150,7 +150,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
 }
 
 func (app *App) validateGhVersion() error {
-	output, err := app.OSCommand.RunCommandWithOutput("gh --version")
+	output, err := app.OSCommand.Cmd.New("gh --version").RunWithOutput()
 	// if we get an error anywhere here we'll show the same status
 	minVersionError := errors.New(app.Tr.MinGhVersionError)
 	if err != nil {
diff --git a/pkg/commands/git.go b/pkg/commands/git.go
index f6812e254..31ba2f42a 100644
--- a/pkg/commands/git.go
+++ b/pkg/commands/git.go
@@ -37,6 +37,7 @@ type GitCommand struct {
 	Tag         *git_commands.TagCommands
 	WorkingTree *git_commands.WorkingTreeCommands
 	Bisect      *git_commands.BisectCommands
+	Gh          *git_commands.GhCommands
 
 	Loaders Loaders
 }
@@ -115,6 +116,7 @@ func NewGitCommandAux(
 	patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff)
 	patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchManager)
 	bisectCommands := git_commands.NewBisectCommands(gitCommon)
+	ghCommands := git_commands.NewGhCommand(gitCommon)
 
 	return &GitCommand{
 		Branch:      branchCommands,
@@ -133,6 +135,7 @@ func NewGitCommandAux(
 		Tag:         tagCommands,
 		Bisect:      bisectCommands,
 		WorkingTree: workingTreeCommands,
+		Gh:          ghCommands,
 		Loaders: Loaders{
 			Branches:      loaders.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchName, configCommands),
 			CommitFiles:   loaders.NewCommitFileLoader(cmn, cmd),
diff --git a/pkg/commands/git_commands/gh.go b/pkg/commands/git_commands/gh.go
new file mode 100644
index 000000000..30d580aa4
--- /dev/null
+++ b/pkg/commands/git_commands/gh.go
@@ -0,0 +1,128 @@
+package git_commands
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
+	"github.com/jesseduffield/lazygit/pkg/commands/models"
+)
+
+type GhCommands struct {
+	*GitCommon
+}
+
+func NewGhCommand(gitCommon *GitCommon) *GhCommands {
+	return &GhCommands{
+		GitCommon: gitCommon,
+	}
+}
+
+// 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()
+
+}
+
+// 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()
+}
+
+func (self *GhCommands) prList() (string, error) {
+	return self.cmd.NewShell("gh pr list --limit 100 --state all --json state,url,number,headRefName,headRepositoryOwner").RunWithOutput()
+}
+
+func (self *GhCommands) GithubMostRecentPRs() ([]*models.GithubPullRequest, error) {
+	commandOutput, err := self.prList()
+	if err != nil {
+		return nil, err
+	}
+
+	prs := []*models.GithubPullRequest{}
+	err = json.Unmarshal([]byte(commandOutput), &prs)
+	if err != nil {
+		return nil, err
+	}
+
+	return prs, nil
+}
+
+func GenerateGithubPullRequestMap(prs []*models.GithubPullRequest, branches []*models.Branch, remotes []*models.Remote) (map[*models.Branch]*models.GithubPullRequest, error) {
+	res := map[*models.Branch]*models.GithubPullRequest{}
+
+	if len(prs) == 0 {
+		return res, nil
+	}
+
+	remotesToOwnersMap, err := getRemotesToOwnersMap(remotes)
+
+	if len(remotesToOwnersMap) == 0 {
+		return res, err
+	}
+
+	prWithStringKey := map[string]models.GithubPullRequest{}
+
+	for _, pr := range prs {
+		prWithStringKey[pr.UserName()+":"+pr.BranchName()] = *pr
+	}
+
+	for _, branch := range branches {
+		if !branch.IsTrackingRemote() || branch.UpstreamBranch == "" {
+			continue
+		}
+
+		owner, foundRemoteOwner := remotesToOwnersMap[branch.UpstreamRemote]
+		if !foundRemoteOwner {
+			continue
+		}
+
+		pr, hasPr := prWithStringKey[owner+":"+branch.UpstreamBranch]
+
+		if !hasPr {
+			continue
+		}
+
+		res[branch] = &pr
+	}
+
+	return res, nil
+}
+
+func GetRepoInfoFromURL(url string) hosting_service.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 hosting_service.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 hosting_service.RepoInformation{
+		Owner:      owner,
+		Repository: repo,
+	}
+}
+
+func getRemotesToOwnersMap(remotes []*models.Remote) (map[string]string, error) {
+	res := map[string]string{}
+	for _, remote := range remotes {
+		if len(remote.Urls) == 0 {
+			continue
+		}
+
+		res[remote.Name] = GetRepoInfoFromURL(remote.Urls[0]).Owner
+	}
+	return res, nil
+}
diff --git a/pkg/commands/github.go b/pkg/commands/github.go
deleted file mode 100644
index 65489143f..000000000
--- a/pkg/commands/github.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package commands
-
-import (
-	"encoding/json"
-
-	"github.com/jesseduffield/lazygit/pkg/commands/models"
-)
-
-func (c *GitCommand) GithubMostRecentPRs() ([]*models.GithubPullRequest, error) {
-	commandOutput, err := c.OSCommand.RunCommandWithOutput("gh pr list --limit 50 --state all --json state,url,number,headRefName,headRepositoryOwner")
-	if err != nil {
-		return nil, err
-	}
-
-	prs := []*models.GithubPullRequest{}
-	err = json.Unmarshal([]byte(commandOutput), &prs)
-	if err != nil {
-		return nil, err
-	}
-
-	return prs, nil
-}
-
-func (c *GitCommand) GenerateGithubPullRequestMap(prs []*models.GithubPullRequest, branches []*models.Branch, remotes []*models.Remote) (map[*models.Branch]*models.GithubPullRequest, error) {
-	res := map[*models.Branch]*models.GithubPullRequest{}
-
-	if len(prs) == 0 {
-		return res, nil
-	}
-
-	remotesToOwnersMap, err := c.GetRemotesToOwnersMap(remotes)
-
-	if len(remotesToOwnersMap) == 0 {
-		return res, err
-	}
-
-	prWithStringKey := map[string]models.GithubPullRequest{}
-
-	for _, pr := range prs {
-		prWithStringKey[pr.UserName()+":"+pr.BranchName()] = *pr
-	}
-
-	for _, branch := range branches {
-		if !branch.IsTrackingRemote() || branch.BranchName() == "" {
-			continue
-		}
-
-		owner, foundRemoteOwner := remotesToOwnersMap[branch.RemoteName()]
-		if branch.BranchName() == "" || !foundRemoteOwner {
-			continue
-		}
-
-		pr, hasPr := prWithStringKey[owner+":"+branch.BranchName()]
-		if !hasPr {
-			continue
-		}
-
-		res[branch] = &pr
-	}
-
-	return res, nil
-}
diff --git a/pkg/commands/github_test.go b/pkg/commands/github_test.go
deleted file mode 100644
index d8c09f970..000000000
--- a/pkg/commands/github_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-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   []*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",
-			"[]",
-			[]*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"
-				}
-			}]`,
-			[]*models.GithubPullRequest{{
-				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/models/branch.go b/pkg/commands/models/branch.go
index 7bd109d97..3cdf5ad6d 100644
--- a/pkg/commands/models/branch.go
+++ b/pkg/commands/models/branch.go
@@ -1,7 +1,5 @@
 package models
 
-import "strings"
-
 // Branch : A git branch
 // duplicating this for now
 type Branch struct {
@@ -57,15 +55,3 @@ func (b *Branch) HasCommitsToPull() bool {
 func (b *Branch) IsRealBranch() bool {
 	return b.Pushables != "" && b.Pullables != ""
 }
-func (b *Branch) RemoteName() string {
-	return strings.SplitN(b.UpstreamName, "/", 2)[0]
-}
-
-func (b *Branch) BranchName() string {
-	remoteAndBranch := strings.SplitN(b.UpstreamName, "/", 2)
-	if len(remoteAndBranch) != 2 {
-		return ""
-	}
-
-	return strings.SplitN(b.UpstreamName, "/", 2)[1]
-}
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index 5d03daba0..9efc73d90 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -71,7 +71,7 @@ type GitConfig struct {
 	OverrideGpg         bool                          `yaml:"overrideGpg"`
 	DisableForcePushing bool                          `yaml:"disableForcePushing"`
 	CommitPrefixes      map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
-	// this shoudl really be under 'gui', not 'git'
+	// this should really be under 'gui', not 'git'
 	ParseEmoji      bool      `yaml:"parseEmoji"`
 	Log             LogConfig `yaml:"log"`
 	EnableGhCommand bool      `yaml:"enableGhCommand"`
@@ -376,6 +376,7 @@ func GetDefaultConfig() *UserConfig {
 			CommitPrefixes:      map[string]CommitPrefixConfig(nil),
 			ParseEmoji:          false,
 			DiffContextSize:     3,
+			EnableGhCommand:     false,
 		},
 		Refresher: RefresherConfig{
 			RefreshInterval: 10,
diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go
index c2c44f273..7e68fdb17 100644
--- a/pkg/gui/branches_panel.go
+++ b/pkg/gui/branches_panel.go
@@ -75,7 +75,7 @@ func (gui *Gui) refreshBranches() {
 }
 
 func (gui *Gui) refreshGithubPullRequests() {
-	_, err := gui.GitCommand.RunCommandWithOutput("git config --local --get-regexp .gh-resolved$")
+	err := gui.Git.Gh.BaseRepo()
 	if err == nil {
 		_ = gui.setGithubPullRequests()
 		return
@@ -89,8 +89,7 @@ func (gui *Gui) refreshGithubPullRequests() {
 		findSuggestionsFunc: gui.getRemoteRepoSuggestionsFunc(),
 		handleConfirm: func(repository string) error {
 			return gui.WithWaitingStatus(gui.Tr.LcSelectingRemote, func() error {
-				// ex git config --local --add "remote.origin.gh-resolved" "jesseduffield/lazygit"
-				_, err := gui.GitCommand.RunCommandWithOutput(fmt.Sprintf("git config --local --add \"remote.origin.gh-resolved\" \"%s\"", repository))
+				_, err := gui.Git.Gh.SetBaseRepo(repository)
 				if err != nil {
 					return err
 				}
@@ -107,7 +106,8 @@ func (gui *Gui) refreshGithubPullRequests() {
 }
 
 func (gui *Gui) setGithubPullRequests() error {
-	prs, err := gui.GitCommand.GithubMostRecentPRs()
+	prs, err := gui.Git.Gh.GithubMostRecentPRs()
+
 	if err != nil {
 		return gui.surfaceError(err)
 	}
diff --git a/pkg/gui/find_suggestions.go b/pkg/gui/find_suggestions.go
index 5864eb178..56a27ee94 100644
--- a/pkg/gui/find_suggestions.go
+++ b/pkg/gui/find_suggestions.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/jesseduffield/lazygit/pkg/commands"
+	"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"
@@ -60,7 +60,7 @@ func (gui *Gui) getRemoteRepoNames() []string {
 		if len(remote.Urls) == 0 {
 			continue
 		}
-		info := commands.GetRepoInfoFromURL(remote.Urls[0])
+		info := git_commands.GetRepoInfoFromURL(remote.Urls[0])
 		result = append(result, fmt.Sprintf("%s/%s", info.Owner, info.Repository))
 	}
 
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index ed41537c2..375f7c0d4 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -313,6 +313,7 @@ type guiState struct {
 	Tags           []*models.Tag
 	MenuItems      []*menuItem
 	BisectInfo     *git_commands.BisectInfo
+	GithubState    *GithubState
 
 	Updating          bool
 	Panels            *panelStates
@@ -789,20 +790,20 @@ func (gui *Gui) setColorScheme() error {
 	return nil
 }
 
-// func (gui *Gui) GetPr(branch *models.Branch) (*models.GithubPullRequest, bool, error) {
-// 	prs, err := gui.GitCommand.GenerateGithubPullRequestMap(
-// 		gui.State.GithubState.RecentPRs,
-// 		[]*models.Branch{branch},
-// 		gui.State.Remotes,
-// 	)
-// 	if err != nil {
-// 		return nil, false, err
-// 	}
+func (gui *Gui) GetPr(branch *models.Branch) (*models.GithubPullRequest, bool, error) {
+	prs, err := git_commands.GenerateGithubPullRequestMap(
+		gui.State.GithubState.RecentPRs,
+		[]*models.Branch{branch},
+		gui.State.Remotes,
+	)
+	if err != nil {
+		return nil, false, err
+	}
 
-// 	pr, hasPr := prs[branch]
+	pr, hasPr := prs[branch]
 
-// 	return pr, hasPr, nil
-// }
+	return pr, hasPr, nil
+}
 
 func (gui *Gui) OnUIThread(f func() error) {
 	gui.g.Update(func(*gocui.Gui) error {
diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go
index 7d9fb7947..3fde1edfd 100644
--- a/pkg/gui/list_context_config.go
+++ b/pkg/gui/list_context_config.go
@@ -69,7 +69,7 @@ func (gui *Gui) branchesListContext() IListContext {
 		OnRenderToMain:  OnFocusWrapper(gui.branchesRenderToMain),
 		Gui:             gui,
 		GetDisplayStrings: func(startIdx int, length int) [][]string {
-			prs, err := gui.GitCommand.GenerateGithubPullRequestMap(gui.State.GithubState.RecentPRs, gui.State.Branches, gui.State.Remotes)
+			prs, err := git_commands.GenerateGithubPullRequestMap(gui.State.GithubState.RecentPRs, gui.State.Branches, gui.State.Remotes)
 			if err != nil {
 				panic(err)
 			}
diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go
index 4dac7328e..1ace50fab 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/jesseduffield/lazygit/pkg/commands/models"
@@ -12,7 +13,10 @@ import (
 
 var branchPrefixColorCache = make(map[string]style.TextStyle)
 
-func GetBranchListDisplayStrings(branches []*models.Branch, fullDescription bool, diffName string) [][]string {
+func GetBranchListDisplayStrings(
+	branches []*models.Branch,
+	prs map[*models.Branch]*models.GithubPullRequest,
+	fullDescription bool, diffName string) [][]string {
 	lines := make([][]string, len(branches))
 
 	for i := range branches {
@@ -50,11 +54,11 @@ func getBranchDisplayStrings(
 
 	res := []string{recencyColor.Sprint(b.Recency)}
 	pr, hasPr := prs[b]
+
 	res = append(res, coloredPrNumber(pr, hasPr), coloredName)
 
 	if fullDescription {
-		return append(
-			res,
+		res = append(res,
 			fmt.Sprintf("%s %s",
 				style.FgYellow.Sprint(b.UpstreamRemote),
 				style.FgYellow.Sprint(b.UpstreamBranch),
@@ -102,3 +106,18 @@ func BranchStatus(branch *models.Branch) string {
 func SetCustomBranches(customBranchColors map[string]string) {
 	branchPrefixColorCache = utils.SetCustomColors(customBranchColors)
 }
+
+func coloredPrNumber(pr *models.GithubPullRequest, hasPr bool) string {
+	if hasPr {
+		colour := style.FgMagenta // = state MERGED
+		switch pr.State {
+		case "OPEN":
+			colour = style.FgGreen
+		case "CLOSED":
+			colour = style.FgRed
+		}
+		return colour.Sprint("#" + strconv.Itoa(pr.Number))
+	}
+
+	return ("")
+}
diff --git a/pkg/gui/pull_request_menu_panel.go b/pkg/gui/pull_request_menu_panel.go
index cd39b0c6a..276074072 100644
--- a/pkg/gui/pull_request_menu_panel.go
+++ b/pkg/gui/pull_request_menu_panel.go
@@ -45,7 +45,7 @@ func (gui *Gui) createOrOpenPullRequestMenu(selectedBranch *models.Branch, check
 
 	if hasPr {
 		menuItems = append(menuItems, &menuItem{
-			displayString: gui.GitCommand.Tr.MustSpecifyOriginError + strconv.Itoa(pr.Number),
+			displayString: gui.Git.Gh.Tr.MustSpecifyOriginError + strconv.Itoa(pr.Number),
 			onPress: func() error {
 				return gui.OSCommand.OpenLink(pr.Url)
 			},
diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go
index 060689db7..580a08205 100644
--- a/pkg/i18n/chinese.go
+++ b/pkg/i18n/chinese.go
@@ -441,9 +441,6 @@ func chineseTranslationSet() TranslationSet {
 		LcSelectBranch:                      "选择分支",
 		CreatingPullRequestAtUrl:            "在 URL 创建抓取请求: %s",
 		OpenPr:                              "公开公关 #",
-
-		Spans: Spans{
-		ConfirmRevertCommit:                 "您确定要撤消 {{.selectedCommit}} 吗?",
 		Actions: Actions{
 			// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
 			CheckoutCommit:                    "检出提交",
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index e1d7c5b26..222ad4d89 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -440,8 +440,6 @@ type TranslationSet struct {
 	DecreaseContextInDiffView           string
 	CreatePullRequestOptions            string
 	LcCreatePullRequestOptions          string
-	LcDefaultBranch                     string
-	LcSelectBranch                      string
 	CreatePullRequest                   string
 	SelectConfigFile                    string
 	NoConfigFileFoundErr                string