Compare commits

...

38 Commits

Author SHA1 Message Date
Jesse Duffield
79a72fe7a6 refactor 2022-09-22 09:03:11 -07:00
Yuki Osaki
ac8e76bee0 Merge branch 'master' into gh-integration 2022-09-07 11:49:45 +09:00
Yuki Osaki
c06974b879 Merge branch 'master' into gh-integration 2022-08-22 11:50:50 +09:00
Yuki Osaki
b5578b3816 fix reload issue 2022-08-19 13:11:43 +09:00
Yuki Osaki
93bbea1221 fix layout 2022-08-19 12:18:35 +09:00
Yuki Osaki
f277ea2350 fix conflict 2022-08-16 22:07:42 +09:00
Yuki Osaki
4a31f3fd50 Merge branch 'master' into gh-integration 2022-08-14 20:48:42 +09:00
Yuki Osaki
c3d2722cb8 fix conflicts 2022-02-25 11:50:59 +09:00
Yuki Osaki
ed4e7ac960 Merge branch 'master' into gh-integration 2022-02-25 09:01:03 +09:00
Yuki Osaki
bf355bc0b5 fix based on the reviews 2021-11-10 00:19:39 +09:00
Yuki Osaki
27a12fe2ea Update pkg/commands/github.go
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2021-11-09 21:21:59 +09:00
Yuki Osaki
37aa558d34 Merge branch 'master' into gh-integration 2021-11-06 12:49:36 +09:00
Yuki Osaki
588bd95118 handle when repo is not set 2021-11-03 00:14:25 +09:00
Yuki Osaki
2b8c959cb0 add Gh version check 2021-11-02 22:04:22 +09:00
Yuki Osaki
4fead59416 Merge branch 'master' into gh-integration 2021-11-02 21:11:35 +09:00
Yuki Osaki
2d1c29fa76 Add enableGhCommand flag 2021-11-01 00:00:39 +09:00
Yuki Osaki
66f3978ab5 use surfaceError 2021-10-31 23:48:52 +09:00
Yuki Osaki
c8de14aa9d generate github state struct 2021-10-31 23:39:01 +09:00
Yuki Osaki
d2e75a6359 remove BranchesWithGithubPullRequests check 2021-10-31 23:31:05 +09:00
Yuki Osaki
b36f6db521 refactor logic 2021-10-31 23:16:04 +09:00
Yuki Osaki
769a6cd069 add local 2021-10-31 23:10:05 +09:00
Yuki Osaki
1547b4155e remove refreshStatus 2021-10-31 22:59:32 +09:00
Yuki Osaki
153970fb08 split logic into functions 2021-10-31 22:55:11 +09:00
Yuki Osaki
aed7c29509 move pr color logic to own method 2021-10-31 22:40:39 +09:00
Yuki Osaki
4a1fa4889d Merge branch 'master' into gh-integration 2021-10-31 22:29:37 +09:00
Yuki Osaki
515e2d2fa9 refactor the code 2021-10-31 22:28:03 +09:00
Yuki Osaki
c1f3fd6b3f Update GithubMostRecentPRs method 2021-10-31 15:40:18 +09:00
Yuki Osaki
9d774c2450 Use c.GetRemotes() 2021-10-31 14:34:27 +09:00
Yuki Osaki
04edfc2c63 remove unnecessary check 2021-10-31 14:03:45 +09:00
Yuki Osaki
1bf9d2e30c Merge branch 'master' into gh-integration 2021-10-31 13:59:22 +09:00
Yuki Osaki
502c23dff7 update the logic 2021-10-31 13:58:07 +09:00
Yuki Osaki
61d08dc4af better error handle 2021-10-30 15:58:37 +09:00
Yuki Osaki
a71db3355a update it not to inject the PRs into our branches 2021-10-30 00:37:38 +09:00
Yuki Osaki
7fbf4fa7f7 say 'create / open' instead of 'create / show' 2021-10-29 21:09:31 +09:00
Yuki Osaki
3ed1611d94 Move it to guiState 2021-10-29 21:03:04 +09:00
Yuki Osaki
ada3b528d3 fix conflict 2021-10-29 20:29:41 +09:00
Yuki Osaki
3df068b1fc Merge branch 'master' into gh-integration 2021-10-29 19:19:43 +09:00
mjarkk
c295deaa81 show github pr number in branches list 2021-07-29 12:14:22 +02:00
24 changed files with 570 additions and 114 deletions

View File

@@ -85,6 +85,7 @@ git:
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
parseEmoji: false
enableGhCommand: false
diffContextSize: 3 # how many lines of context are shown around a change in diffs
os:
editCommand: '' # see 'Configuring File Editing' section

View File

@@ -112,22 +112,63 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
if err != nil {
return app, err
}
if app.Gui.Config.GetUserConfig().Git.EnableGhCommand {
if err := app.validateGhVersion(); err != nil {
return nil, err
}
}
return app, nil
}
func (app *App) validateGhVersion() error {
output, err := app.OSCommand.Cmd.New("gh --version").RunWithOutput()
if err != nil {
return fmt.Errorf(app.Tr.FailedToObtainGhVersionError, err.Error())
}
if !isGhVersionValid(output) {
return errors.New(app.Tr.MinGhVersionError)
}
return nil
}
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
minVersionError := errors.New(app.Tr.MinGitVersionError)
if err != nil {
return minVersionError
return fmt.Errorf(app.Tr.FailedToObtainGitVersionError, err.Error())
}
if isGitVersionValid(output) {
return nil
if !isGitVersionValid(output) {
return errors.New(app.Tr.MinGitVersionError)
}
return minVersionError
return nil
}
func isGitVersionValid(versionStr string) bool {

View File

@@ -43,3 +43,43 @@ func TestIsGitVersionValid(t *testing.T) {
})
}
}
func TestIsValidGhVersion(t *testing.T) {
type scenario struct {
versionStr string
expectedResult bool
}
scenarios := []scenario{
{
"",
false,
},
{
`gh version 1.0.0 (2020-08-23)
https://github.com/cli/cli/releases/tag/v1.0.0`,
false,
},
{
`gh version 2.0.0 (2021-08-23)
https://github.com/cli/cli/releases/tag/v2.0.0`,
true,
},
{
`gh version 1.1.0 (2021-10-14)
https://github.com/cli/cli/releases/tag/v1.1.0
A new release of gh is available: 1.1.0 → v2.2.0
To upgrade, run: brew update && brew upgrade gh
https://github.com/cli/cli/releases/tag/v2.2.0`,
false,
},
}
for _, s := range scenarios {
t.Run(s.versionStr, func(t *testing.T) {
result := isGhVersionValid(s.versionStr)
assert.Equal(t, result, s.expectedResult)
})
}
}

View File

@@ -22,22 +22,24 @@ import (
// GitCommand is our main git interface
type GitCommand struct {
Branch *git_commands.BranchCommands
Commit *git_commands.CommitCommands
Config *git_commands.ConfigCommands
Custom *git_commands.CustomCommands
File *git_commands.FileCommands
Flow *git_commands.FlowCommands
Patch *git_commands.PatchCommands
Rebase *git_commands.RebaseCommands
Remote *git_commands.RemoteCommands
Stash *git_commands.StashCommands
Status *git_commands.StatusCommands
Submodule *git_commands.SubmoduleCommands
Sync *git_commands.SyncCommands
Tag *git_commands.TagCommands
WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands
Branch *git_commands.BranchCommands
Commit *git_commands.CommitCommands
Config *git_commands.ConfigCommands
Custom *git_commands.CustomCommands
File *git_commands.FileCommands
Flow *git_commands.FlowCommands
Patch *git_commands.PatchCommands
Rebase *git_commands.RebaseCommands
Remote *git_commands.RemoteCommands
Stash *git_commands.StashCommands
Status *git_commands.StatusCommands
Submodule *git_commands.SubmoduleCommands
Sync *git_commands.SyncCommands
Tag *git_commands.TagCommands
WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands
Gh *git_commands.GhCommands
HostingService *git_commands.HostingService
Loaders Loaders
}
@@ -119,24 +121,28 @@ 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)
hostingServiceCommands := git_commands.NewHostingServiceCommand(gitCommon)
return &GitCommand{
Branch: branchCommands,
Commit: commitCommands,
Config: configCommands,
Custom: customCommands,
File: fileCommands,
Flow: flowCommands,
Patch: patchCommands,
Rebase: rebaseCommands,
Remote: remoteCommands,
Stash: stashCommands,
Status: statusCommands,
Submodule: submoduleCommands,
Sync: syncCommands,
Tag: tagCommands,
Bisect: bisectCommands,
WorkingTree: workingTreeCommands,
Branch: branchCommands,
Commit: commitCommands,
Config: configCommands,
Custom: customCommands,
File: fileCommands,
Flow: flowCommands,
Patch: patchCommands,
Rebase: rebaseCommands,
Remote: remoteCommands,
Stash: stashCommands,
Status: statusCommands,
Submodule: submoduleCommands,
Sync: syncCommands,
Tag: tagCommands,
Bisect: bisectCommands,
WorkingTree: workingTreeCommands,
Gh: ghCommands,
HostingService: hostingServiceCommands,
Loaders: Loaders{
Branches: loaders.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchName, configCommands),
CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),

View File

@@ -0,0 +1,146 @@
package git_commands
import (
"encoding/json"
"fmt"
"strings"
"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").Run()
}
// Ex: git config --local --add "remote.origin.gh-resolved" "jesseduffield/lazygit"
func (self *GhCommands) SetBaseRepo(repository string) (string, error) {
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.New(
"gh pr list --limit 500 --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 {
res := map[*models.Branch]*models.GithubPullRequest{}
if len(prs) == 0 {
return res
}
remotesToOwnersMap := getRemotesToOwnersMap(remotes)
if len(remotesToOwnersMap) == 0 {
return res
}
// A PR can be identified by two things: the owner e.g. 'jesseduffield' and the
// branch name e.g. 'feature/my-feature'. The owner might be different
// to the owner of the repo if the PR is from a fork of that repo.
type prKey struct {
owner string
branchName string
}
prByKey := map[prKey]models.GithubPullRequest{}
for _, pr := range prs {
prByKey[prKey{owner: pr.UserName(), branchName: pr.BranchName()}] = *pr
}
for _, branch := range branches {
if !branch.IsTrackingRemote() {
continue
}
// TODO: support branches whose UpstreamRemote contains a full git
// URL rather than just a remote name.
owner, foundRemoteOwner := remotesToOwnersMap[branch.UpstreamRemote]
if !foundRemoteOwner {
continue
}
pr, hasPr := prByKey[prKey{owner: owner, branchName: branch.UpstreamBranch}]
if !hasPr {
continue
}
res[branch] = &pr
}
return res
}
func getRemotesToOwnersMap(remotes []*models.Remote) map[string]string {
res := map[string]string{}
for _, remote := range remotes {
if len(remote.Urls) == 0 {
continue
}
res[remote.Name] = GetRepoInfoFromURL(remote.Urls[0]).Owner
}
return res
}
type RepoInformation struct {
Owner string
Repository string
}
// TODO: move this into hosting_service.go
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,
}
}

View File

@@ -0,0 +1,34 @@
package git_commands
import "github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
// a hosting service is something like github, gitlab, bitbucket etc
type HostingService struct {
*GitCommon
}
func NewHostingServiceCommand(gitCommon *GitCommon) *HostingService {
return &HostingService{
GitCommon: gitCommon,
}
}
func (self *HostingService) GetPullRequestURL(from string, to string) (string, error) {
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetPullRequestURL(from, to)
}
func (self *HostingService) GetCommitURL(commitSha string) (string, error) {
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetCommitURL(commitSha)
}
func (self *HostingService) GetRepoNameFromRemoteURL(remoteURL string) (string, error) {
return self.getHostingServiceMgr(remoteURL).GetRepoName()
}
// getting this on every request rather than storing it in state in case our remoteURL changes
// from one invocation to the next. Note however that we're currently caching config
// results so we might want to invalidate the cache here if it becomes a problem.
func (self *HostingService) getHostingServiceMgr(remoteURL string) *hosting_service.HostingServiceMgr {
configServices := self.UserConfig.Services
return hosting_service.NewHostingServiceMgr(self.Log, self.Tr, remoteURL, configServices)
}

View File

@@ -6,7 +6,11 @@ var defaultUrlRegexStrings = []string{
`^(?:https?|ssh)://[^/]+/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^git@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
}
var defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
var (
defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
defaultRepoNameTemplate = "{{.owner}}/{{.repo}}"
)
// we've got less type safety using go templates but this lends itself better to
// users adding custom service definitions in their config
@@ -17,6 +21,7 @@ var githubServiceDef = ServiceDefinition{
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var bitbucketServiceDef = ServiceDefinition{
@@ -28,7 +33,8 @@ var bitbucketServiceDef = ServiceDefinition{
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^.*@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: defaultRepoURLTemplate,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var gitLabServiceDef = ServiceDefinition{
@@ -38,6 +44,7 @@ var gitLabServiceDef = ServiceDefinition{
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var azdoServiceDef = ServiceDefinition{
@@ -50,6 +57,8 @@ var azdoServiceDef = ServiceDefinition{
`^https://.*@dev.azure.com/(?P<org>.*?)/(?P<project>.*?)/_git/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: "https://{{.webDomain}}/{{.org}}/{{.project}}/_git/{{.repo}}",
// TODO: verify this is actually correct
repoNameTemplate: "{{.org}}/{{.project}}/{{.repo}}",
}
var bitbucketServerServiceDef = ServiceDefinition{
@@ -62,6 +71,8 @@ var bitbucketServerServiceDef = ServiceDefinition{
`^https://.*/scm/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
// TODO: verify this is actually correct
repoNameTemplate: "{{.project}}/{{.repo}}",
}
var serviceDefinitions = []ServiceDefinition{

View File

@@ -61,6 +61,18 @@ func (self *HostingServiceMgr) GetCommitURL(commitSha string) (string, error) {
return pullRequestURL, nil
}
// e.g. 'jesseduffield/lazygit'
func (self *HostingServiceMgr) GetRepoName() (string, error) {
gitService, err := self.getService()
if err != nil {
return "", err
}
repoName := gitService.repoName
return repoName, nil
}
func (self *HostingServiceMgr) getService() (*Service, error) {
serviceDomain, err := self.getServiceDomain(self.remoteURL)
if err != nil {
@@ -72,8 +84,14 @@ func (self *HostingServiceMgr) getService() (*Service, error) {
return nil, err
}
repoName, err := serviceDomain.serviceDefinition.getRepoNameFromRemoteURL(self.remoteURL)
if err != nil {
return nil, err
}
return &Service{
repoURL: repoURL,
repoName: repoName,
ServiceDefinition: serviceDomain.serviceDefinition,
}, nil
}
@@ -147,24 +165,45 @@ type ServiceDefinition struct {
regexStrings []string
// can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex
repoURLTemplate string
repoURLTemplate string
repoNameTemplate string
}
func (self ServiceDefinition) getRepoURLFromRemoteURL(url string, webDomain string) (string, error) {
matches, err := self.parseRemoteUrl(url)
if err != nil {
return "", err
}
matches["webDomain"] = webDomain
return utils.ResolvePlaceholderString(self.repoURLTemplate, matches), nil
}
func (self ServiceDefinition) getRepoNameFromRemoteURL(url string) (string, error) {
matches, err := self.parseRemoteUrl(url)
if err != nil {
return "", err
}
return utils.ResolvePlaceholderString(self.repoNameTemplate, matches), nil
}
func (self ServiceDefinition) parseRemoteUrl(url string) (map[string]string, error) {
for _, regexStr := range self.regexStrings {
re := regexp.MustCompile(regexStr)
input := utils.FindNamedMatches(re, url)
if input != nil {
input["webDomain"] = webDomain
return utils.ResolvePlaceholderString(self.repoURLTemplate, input), nil
matches := utils.FindNamedMatches(re, url)
if matches != nil {
return matches, nil
}
}
return "", errors.New("Failed to parse repo information from url")
return nil, errors.New("Failed to parse repo information from url")
}
type Service struct {
repoURL string
// e.g. 'jesseduffield/lazygit'
repoName string
ServiceDefinition
}

View File

@@ -0,0 +1,25 @@
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"`
}
func (pr *GithubPullRequest) UserName() string {
// e.g. 'jesseduffield'
return pr.HeadRepositoryOwner.Login
}
func (pr *GithubPullRequest) BranchName() string {
// e.g. 'feature/my-feature'
return pr.HeadRefName
}
type GithubRepositoryOwner struct {
ID string `json:"id"`
Name string `json:"name"`
Login string `json:"login"`
}

View File

@@ -85,6 +85,7 @@ type GitConfig struct {
// this should really be under 'gui', not 'git'
ParseEmoji bool `yaml:"parseEmoji"`
Log LogConfig `yaml:"log"`
EnableGhCommand bool `yaml:"enableGhCommand"`
DiffContextSize int `yaml:"diffContextSize"`
}
@@ -403,6 +404,7 @@ func GetDefaultConfig() *UserConfig {
CommitPrefixes: map[string]CommitPrefixConfig(nil),
ParseEmoji: false,
DiffContextSize: 3,
EnableGhCommand: false,
},
Refresher: RefresherConfig{
RefreshInterval: 10,

View File

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

View File

@@ -23,10 +23,9 @@ func (gui *Gui) resetControllers() {
)
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper)
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.refreshSuggestions)
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, gui.git, model, gui.refreshSuggestions)
gui.helpers = &helpers.Helpers{
Refs: refsHelper,
Host: helpers.NewHostHelper(helperCommon, gui.git),
PatchBuilding: helpers.NewPatchBuildingHelper(helperCommon, gui.git, gui.State.Contexts),
Bisect: helpers.NewBisectHelper(helperCommon, gui.git),
Suggestions: suggestionsHelper,

View File

@@ -150,7 +150,7 @@ func (self *BasicCommitsController) copyCommitSHAToClipboard(commit *models.Comm
}
func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Commit) error {
url, err := self.helpers.Host.GetCommitURL(commit.Sha)
url, err := self.git.HostingService.GetCommitURL(commit.Sha)
if err != nil {
return err
}
@@ -212,7 +212,7 @@ func (self *BasicCommitsController) copyCommitMessageToClipboard(commit *models.
}
func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error {
url, err := self.helpers.Host.GetCommitURL(commit.Sha)
url, err := self.git.HostingService.GetCommitURL(commit.Sha)
if err != nil {
return self.c.Error(err)
}

View File

@@ -195,7 +195,7 @@ func (self *BranchesController) copyPullRequestURL() error {
return self.c.Error(errors.New(self.c.Tr.NoBranchOnRemote))
}
url, err := self.helpers.Host.GetPullRequestURL(branch.Name, "")
url, err := self.git.HostingService.GetPullRequestURL(branch.Name, "")
if err != nil {
return self.c.Error(err)
}
@@ -462,7 +462,7 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
}
func (self *BranchesController) createPullRequest(from string, to string) error {
url, err := self.helpers.Host.GetPullRequestURL(from, to)
url, err := self.git.HostingService.GetPullRequestURL(from, to)
if err != nil {
return self.c.Error(err)
}

View File

@@ -10,7 +10,6 @@ type Helpers struct {
MergeAndRebase *MergeAndRebaseHelper
MergeConflicts *MergeConflictsHelper
CherryPick *CherryPickHelper
Host *HostHelper
PatchBuilding *PatchBuildingHelper
GPG *GpgHelper
Upstream *UpstreamHelper
@@ -27,7 +26,6 @@ func NewStubHelpers() *Helpers {
MergeAndRebase: &MergeAndRebaseHelper{},
MergeConflicts: &MergeConflictsHelper{},
CherryPick: &CherryPickHelper{},
Host: &HostHelper{},
PatchBuilding: &PatchBuildingHelper{},
GPG: &GpgHelper{},
Upstream: &UpstreamHelper{},

View File

@@ -1,46 +0,0 @@
package helpers
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// this helper just wraps our hosting_service package
type IHostHelper interface {
GetPullRequestURL(from string, to string) (string, error)
GetCommitURL(commitSha string) (string, error)
}
type HostHelper struct {
c *types.HelperCommon
git *commands.GitCommand
}
func NewHostHelper(
c *types.HelperCommon,
git *commands.GitCommand,
) *HostHelper {
return &HostHelper{
c: c,
git: git,
}
}
func (self *HostHelper) GetPullRequestURL(from string, to string) (string, error) {
return self.getHostingServiceMgr().GetPullRequestURL(from, to)
}
func (self *HostHelper) GetCommitURL(commitSha string) (string, error) {
return self.getHostingServiceMgr().GetCommitURL(commitSha)
}
// getting this on every request rather than storing it in state in case our remoteURL changes
// from one invocation to the next. Note however that we're currently caching config
// results so we might want to invalidate the cache here if it becomes a problem.
func (self *HostHelper) getHostingServiceMgr() *hosting_service.HostingServiceMgr {
remoteUrl := self.git.Config.GetRemoteURL()
configServices := self.c.UserConfig.Services
return hosting_service.NewHostingServiceMgr(self.c.Log, self.c.Tr, remoteUrl, configServices)
}

View File

@@ -5,6 +5,7 @@ import (
"os"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
@@ -36,6 +37,7 @@ type SuggestionsHelper struct {
c *types.HelperCommon
model *types.Model
git *commands.GitCommand
refreshSuggestionsFn func()
}
@@ -43,11 +45,13 @@ var _ ISuggestionsHelper = &SuggestionsHelper{}
func NewSuggestionsHelper(
c *types.HelperCommon,
git *commands.GitCommand,
model *types.Model,
refreshSuggestionsFn func(),
) *SuggestionsHelper {
return &SuggestionsHelper{
c: c,
git: git,
model: model,
refreshSuggestionsFn: refreshSuggestionsFn,
}
@@ -80,6 +84,30 @@ 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
}
repoName, err := self.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

@@ -286,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),
@@ -773,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
}

View File

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

View File

@@ -2,6 +2,7 @@ package presentation
import (
"fmt"
"strconv"
"strings"
"github.com/jesseduffield/generics/slices"
@@ -15,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
@@ -48,7 +51,11 @@ 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]
res = append(res, coloredPrNumber(pr, hasPr), coloredName)
if fullDescription {
res = append(
res,
@@ -124,3 +131,24 @@ func BranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string {
func SetCustomBranches(customBranchColors map[string]string) {
branchPrefixColorCache = utils.SetCustomColors(customBranchColors)
}
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

@@ -164,21 +164,38 @@ func (gui *Gui) Refresh(options types.RefreshOptions) error {
return nil
}
// during startup, the bottleneck is fetching the reflog entries. We need these
// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE.
// In the initial phase we don't get any reflog commits, but we asynchronously get them
// and refresh the branches after that
func (gui *Gui) refreshReflogCommitsConsideringStartup() {
// during startup, the bottleneck is fetching the reflog entries and Github PRs, both of which affect the contents of the branches view.
// So we have two phases: INITIAL, and COMPLETE.
// In the initial phase we asynchronously refresh the dependencies (reflog entries and Github PRs) and then refresh the branches view.
// After that, we synchronously refresh the dependencies
func (gui *Gui) refreshBranchDependenciesConsideringStartup() {
refreshDeps := func() {
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
_ = gui.refreshReflogCommits()
wg.Done()
}()
go func() {
_ = gui.refreshGithubPullRequests()
wg.Done()
}()
wg.Wait()
}
switch gui.State.StartupStage {
case INITIAL:
go utils.Safe(func() {
_ = gui.refreshReflogCommits()
refreshDeps()
gui.refreshBranches()
gui.State.StartupStage = COMPLETE
})
case COMPLETE:
_ = gui.refreshReflogCommits()
refreshDeps()
}
}
@@ -190,7 +207,7 @@ func (gui *Gui) refreshCommits() {
wg.Add(2)
go utils.Safe(func() {
gui.refreshReflogCommitsConsideringStartup()
gui.refreshBranchDependenciesConsideringStartup()
gui.refreshBranches()
wg.Done()
@@ -711,3 +728,61 @@ func (gui *Gui) refreshMergePanel(isFocused bool) error {
},
})
}
func (gui *Gui) refreshGithubPullRequests() error {
if !gui.Config.GetUserConfig().Git.EnableGhCommand {
return nil
}
if err := gui.git.Gh.BaseRepo(); err == nil {
if err := gui.setGithubPullRequests(); err != nil {
return gui.c.Error(err)
}
return nil
}
// when config not exists
err := gui.refreshRemotes()
if err != nil {
return err
}
_ = 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 {
// `repository` is something like 'jesseduffield/lazygit'
_, err := gui.git.Gh.SetBaseRepo(repository)
if err != nil {
return gui.c.Error(err)
}
if err := gui.setGithubPullRequests(); err != nil {
return gui.c.Error(err)
}
// calling refreshBranches explicitly because it may have taken
// a while for the user to submit their response.
gui.refreshBranches()
return nil
})
},
})
return nil
}
func (gui *Gui) setGithubPullRequests() error {
prs, err := gui.git.Gh.GithubMostRecentPRs()
if err != nil {
return err
}
gui.State.Model.PullRequests = prs
return nil
}

View File

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

View File

@@ -436,6 +436,8 @@ func chineseTranslationSet() TranslationSet {
LcCopiedToClipboard: "复制到剪贴板",
ErrCannotEditDirectory: "无法编辑目录:您只能编辑单个文件",
ErrStageDirWithInlineMergeConflicts: "无法 暂存/取消暂存 包含具有内联合并冲突的文件的目录。请先解决合并冲突",
SelectRemoteRepository: "选择存储库",
LcSelectingRemote: "选择遥控器",
ErrRepositoryMovedOrDeleted: "找不到仓库。它可能已被移动或删除 ¯\\_(ツ)_/¯",
CommandLog: "命令日志",
ToggleShowCommandLog: "切换 显示/隐藏 命令日志",

View File

@@ -356,6 +356,8 @@ type TranslationSet struct {
LcNextScreenMode string
LcPrevScreenMode string
LcStartSearch string
SelectRemoteRepository string
LcSelectingRemote string
Panel string
Keybindings string
LcRenameBranch string
@@ -418,6 +420,9 @@ type TranslationSet struct {
LcBuildingPatch string
LcViewCommits string
MinGitVersionError string
FailedToObtainGitVersionError string
MinGhVersionError string
FailedToObtainGhVersionError string
LcRunningCustomCommandStatus string
LcSubmoduleStashAndReset string
LcAndResetSubmodules string
@@ -996,6 +1001,8 @@ func EnglishTranslationSet() TranslationSet {
LcNextScreenMode: "next screen mode (normal/half/fullscreen)",
LcPrevScreenMode: "prev screen mode",
LcStartSearch: "start search",
LcSelectingRemote: "selecting remote",
SelectRemoteRepository: "Select GitHub base remote repository (for PRs)",
Panel: "Panel",
Keybindings: "Keybindings",
LcRenameBranch: "rename branch",
@@ -1059,6 +1066,9 @@ func EnglishTranslationSet() TranslationSet {
LcBuildingPatch: "building patch",
LcViewCommits: "view commits",
MinGitVersionError: "Git version must be at least 2.0 (i.e. from 2014 onwards). Please upgrade your git version. Alternatively raise an issue at https://github.com/jesseduffield/lazygit/issues for lazygit to be more backwards compatible.",
MinGhVersionError: "GH version must be at least 2.0. Please upgrade your gh version. Alternatively raise an issue at https://github.com/jesseduffield/lazygit/issues for lazygit to be more backwards compatible.",
FailedToObtainGitVersionError: "Failed to obtain git version. Output from running 'git --version' was: %s",
FailedToObtainGhVersionError: "Failed to obtain gh version. Output from running 'gh --version' was: %s",
LcRunningCustomCommandStatus: "running custom command",
LcSubmoduleStashAndReset: "stash uncommitted submodule changes and update",
LcAndResetSubmodules: "and reset submodules",