diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2024-06-03 22:12:09 +1000 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2024-06-29 17:39:38 +1000 |
commit | bcb70119bbb4b48a60fbe0786e026e50acc52cdf (patch) | |
tree | 86517ae73e5554b14112e69e905715eee091baed /pkg | |
parent | 26c3e0d333a0e4404080b9fa3081fc2364367f20 (diff) |
Show github pull request status against branch
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/app/app_test.go | 47 | ||||
-rw-r--r-- | pkg/commands/git.go | 6 | ||||
-rw-r--r-- | pkg/commands/git_commands/github.go | 432 | ||||
-rw-r--r-- | pkg/commands/git_commands/hosting_service.go | 34 | ||||
-rw-r--r-- | pkg/commands/hosting_service/definitions.go | 13 | ||||
-rw-r--r-- | pkg/commands/hosting_service/hosting_service.go | 49 | ||||
-rw-r--r-- | pkg/commands/models/github.go | 24 | ||||
-rw-r--r-- | pkg/config/user_config.go | 3 | ||||
-rw-r--r-- | pkg/gui/background.go | 13 | ||||
-rw-r--r-- | pkg/gui/context/branches_context.go | 2 | ||||
-rw-r--r-- | pkg/gui/controllers.go | 1 | ||||
-rw-r--r-- | pkg/gui/controllers/helpers/helpers.go | 2 | ||||
-rw-r--r-- | pkg/gui/controllers/helpers/refresh_helper.go | 135 | ||||
-rw-r--r-- | pkg/gui/controllers/helpers/suggestions_helper.go | 24 | ||||
-rw-r--r-- | pkg/gui/controllers/helpers/upstream_helper.go | 17 | ||||
-rw-r--r-- | pkg/gui/gui.go | 29 | ||||
-rw-r--r-- | pkg/gui/presentation/branches.go | 54 | ||||
-rw-r--r-- | pkg/gui/types/common.go | 14 | ||||
-rw-r--r-- | pkg/gui/types/refresh.go | 1 | ||||
-rw-r--r-- | pkg/i18n/english.go | 6 |
20 files changed, 862 insertions, 44 deletions
diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go new file mode 100644 index 000000000..82b1cd204 --- /dev/null +++ b/pkg/app/app_test.go @@ -0,0 +1,47 @@ +package app + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +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) + }) + } +} diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 7e7d9354f..742b4c32e 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -38,6 +38,8 @@ type GitCommand struct { Worktree *git_commands.WorktreeCommands Version *git_commands.GitVersion RepoPaths *git_commands.RepoPaths + GitHub *git_commands.GitHubCommands + HostingService *git_commands.HostingService Loaders Loaders } @@ -133,6 +135,8 @@ func NewGitCommandAux( bisectCommands := git_commands.NewBisectCommands(gitCommon) worktreeCommands := git_commands.NewWorktreeCommands(gitCommon) blameCommands := git_commands.NewBlameCommands(gitCommon) + gitHubCommands := git_commands.NewGitHubCommand(gitCommon) + hostingServiceCommands := git_commands.NewHostingServiceCommand(gitCommon) branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands) commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd) @@ -164,6 +168,8 @@ func NewGitCommandAux( WorkingTree: workingTreeCommands, Worktree: worktreeCommands, Version: version, + GitHub: gitHubCommands, + HostingService: hostingServiceCommands, Loaders: Loaders{ BranchLoader: branchLoader, CommitFileLoader: commitFileLoader, diff --git a/pkg/commands/git_commands/github.go b/pkg/commands/git_commands/github.go new file mode 100644 index 000000000..ddce8304a --- /dev/null +++ b/pkg/commands/git_commands/github.go @@ -0,0 +1,432 @@ +package git_commands + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/cli/go-gh/v2/pkg/auth" + gogit "github.com/jesseduffield/go-git/v5" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/samber/lo" + "golang.org/x/sync/errgroup" +) + +type GitHubCommands struct { + *GitCommon +} + +func NewGitHubCommand(gitCommon *GitCommon) *GitHubCommands { + return &GitHubCommands{ + GitCommon: gitCommon, + } +} + +// https://github.com/cli/cli/issues/2300 +func (self *GitHubCommands) BaseRepo() error { + cmdArgs := NewGitCmd("config"). + Arg("--local", "--get-regexp", ".gh-resolved"). + ToArgv() + + return self.cmd.New(cmdArgs).DontLog().Run() +} + +// Ex: git config --local --add "remote.origin.gh-resolved" "jesseduffield/lazygit" +func (self *GitHubCommands) SetBaseRepo(repository string) (string, error) { + cmdArgs := NewGitCmd("config"). + Arg("--local", "--add", "remote.origin.gh-resolved", repository). + ToArgv() + + return self.cmd.New(cmdArgs).DontLog().RunWithOutput() +} + +type Response struct { + Data RepositoryQuery `json:"data"` +} + +type RepositoryQuery struct { + Repository map[string]PullRequest `json:"repository"` +} + +type PullRequest struct { + Edges []PullRequestEdge `json:"edges"` +} + +type PullRequestEdge struct { + Node PullRequestNode `json:"node"` +} + +type PullRequestNode struct { + Title string `json:"title"` + HeadRefName string `json:"headRefName"` + Number int `json:"number"` + Url string `json:"url"` + HeadRepositoryOwner GithubRepositoryOwner `json:"headRepositoryOwner"` + State string `json:"state"` +} + +type GithubRepositoryOwner struct { + Login string `json:"login"` +} + +func fetchPullRequestsQuery(branches []string, owner string, repo string) string { + var queries []string + for i, branch := range branches { + // We're making a sub-query per branch, and arbitrarily labelling each subquery + // as a1, a2, etc. + fieldName := fmt.Sprintf("a%d", i+1) + // TODO: scope down by remote too if we can (right now if you search for master, you can get multiple results back, and all from forks) + queries = append(queries, fmt.Sprintf(`%s: pullRequests(first: 1, headRefName: "%s") { + edges { + node { + title + headRefName + state + number + url + headRepositoryOwner { + login + } + } + } + }`, fieldName, branch)) + } + + queryString := fmt.Sprintf(`{ + repository(owner: "%s", name: "%s") { + %s + } +}`, owner, repo, strings.Join(queries, "\n")) + + return queryString +} + +// FetchRecentPRs fetches recent pull requests using GraphQL. +func (self *GitHubCommands) FetchRecentPRs(branches []string) ([]*models.GithubPullRequest, error) { + repoOwner, repoName, err := self.GetBaseRepoOwnerAndName() + if err != nil { + return nil, err + } + + t := time.Now() + + var g errgroup.Group + results := make(chan []*models.GithubPullRequest) + + // We want at most 5 concurrent requests, but no less than 10 branches per request + concurrency := 5 + minBranchesPerRequest := 10 + branchesPerRequest := max(len(branches)/concurrency, minBranchesPerRequest) + for i := 0; i < len(branches); i += branchesPerRequest { + end := i + branchesPerRequest + if end > len(branches) { + end = len(branches) + } + branchChunk := branches[i:end] + + // Launch a goroutine for each chunk of branches + g.Go(func() error { + prs, err := self.FetchRecentPRsAux(repoOwner, repoName, branchChunk) + if err != nil { + return err + } + results <- prs + return nil + }) + } + + // Close the results channel when all goroutines are done + go func() { + g.Wait() + close(results) + }() + + // Collect results from all goroutines + var allPRs []*models.GithubPullRequest + for prs := range results { + allPRs = append(allPRs, prs...) + } + + if err := g.Wait(); err != nil { + return nil, err + } + + self.Log.Warnf("Fetched PRs in %s", time.Since(t)) + + return allPRs, nil +} + +func (self *GitHubCommands) FetchRecentPRsAux(repoOwner string, repoName string, branches []string) ([]*models.GithubPullRequest, error) { + queryString := fetchPullRequestsQuery(branches, repoOwner, repoName) + escapedQueryString := strconv.Quote(queryString) + + body := fmt.Sprintf(`{"query": %s}`, escapedQueryString) + req, err := http.NewRequest("POST", "https://api.github.com/graphql", bytes.NewBuffer([]byte(body))) + if err != nil { + return nil, err + } + + defaultHost, _ := auth.DefaultHost() + token, _ := auth.TokenForHost(defaultHost) + if token == "" { + return nil, fmt.Errorf("No token found for GitHub") + } + req.Header.Set("Authorization", "token "+token) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyStr := new(bytes.Buffer) + bodyStr.ReadFrom(resp.Body) + return nil, fmt.Errorf("GraphQL query failed with status: %s. Body: %s", resp.Status, bodyStr.String()) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var result Response + err = json.Unmarshal(bodyBytes, &result) + if err != nil { + return nil, err + } + + prs := []*models.GithubPullRequest{} + for _, repoQuery := range result.Data.Repository { + for _, edge := range repoQuery.Edges { + node := edge.Node + pr := &models.GithubPullRequest{ + HeadRefName: node.HeadRefName, + Number: node.Number, + State: node.State, + Url: node.Url, + HeadRepositoryOwner: models.GithubRepositoryOwner{ + Login: node.HeadRepositoryOwner.Login, + }, + } + prs = append(prs, pr) + } + } + + return prs, nil +} + +// returns a map from branch name to pull request +func GenerateGithubPullRequestMap( + prs []*models.GithubPullRequest, + branches []*models.Branch, + remotes []*models.Remote, +) map[string]*models.GithubPullRequest { + res := map[string]*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.Name] = &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, + } +} + +// return <installed>, <valid version> +func (self *GitHubCommands) DetermineGitHubCliState() (bool, bool) { + output, err := self.cmd.New([]string{"gh", "--version"}).DontLog().RunWithOutput() + if err != nil { + // assuming a failure here means that it's not installed + return false, false + } + + if !isGhVersionValid(output) { + return true, false + } + + return true, true +} + +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 (self *GitHubCommands) InGithubRepo() bool { + remotes, err := self.repo.Remotes() + if err != nil { + self.Log.Error(err) + return false + } + + if len(remotes) == 0 { + return false + } + + remote := GetMainRemote(remotes) + + if len(remote.Config().URLs) == 0 { + return false + } + + url := remote.Config().URLs[0] + return strings.Contains(url, "github.com") +} + +func GetMainRemote(remotes []*gogit.Remote) *gogit.Remote { + for _, remote := range remotes { + if remote.Config().Name == "origin" { + return remote + } + } + + // need to sort remotes by name so that this is deterministic + return lo.MinBy(remotes, func(a, b *gogit.Remote) bool { + return a.Config().Name < b.Config().Name + }) +} + +func GetSuggestedRemoteName(remotes []*models.Remote) string { + if len(remotes) == 0 { + return "origin" + } + + for _, remote := range remotes { + if remote.Name == "origin" { + return remote.Name + } + } + + return remotes[0].Name +} + +func (self *GitHubCommands) GetBaseRepoOwnerAndName() (string, string, error) { + remotes, err := self.repo.Remotes() + if err != nil { + return "", "", err + } + + if len(remotes) == 0 { + return "", "", fmt.Errorf("No remotes found") + } + + firstRemote := remotes[0] + if len(firstRemote.Config().URLs) == 0 { + return "", "", fmt.Errorf("No URLs found for remote") + } + + url := firstRemote.Config().URLs[0] + + repoInfo := getRepoInfoFromURL(url) + + return repoInfo.Owner, repoInfo.Repository, nil +} diff --git a/pkg/commands/git_commands/hosting_service.go b/pkg/commands/git_commands/hosting_service.go new file mode 100644 index 000000000..a7295658d --- /dev/null +++ b/pkg/commands/git_commands/hosting_service.go @@ -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) +} diff --git a/pkg/commands/hosting_service/definitions.go b/pkg/commands/hosting_service/definitions.go index ff872cd8c..d431af433 100644 --- a/pkg/commands/hosting_service/definitions.go +++ b/pkg/commands/hosting_service/definitions.go @@ -6,7 +6,11 @@ var defaultUrlRegexStrings = []string{ `^(?:https?|ssh)://[^/]+/(?P<owner>.*)/(?P<repo>.*?)(?:\.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/{{.CommitHash}}", regexStrings: defaultUrlRegexStrings, repoURLTemplate: defaultRepoURLTemplate, + repoNameTemplate: defaultRepoNameTemplate, } var bitbucketServiceDef = ServiceDefinition{ @@ -29,6 +34,7 @@ var bitbucketServiceDef = ServiceDefinition{ `^.*@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`, }, repoURLTemplate: defaultRepoURLTemplate, + repoNameTemplate: defaultRepoNameTemplate, } var gitLabServiceDef = ServiceDefinition{ @@ -38,6 +44,7 @@ var gitLabServiceDef = ServiceDefinition{ commitURL: "/-/commit/{{.CommitHash}}", 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 giteaServiceDef = ServiceDefinition{ diff --git a/pkg/commands/hosting_service/hosting_service.go b/pkg/commands/hosting_service/hosting_service.go index 2e913b654..69c26aa65 100644 --- a/pkg/commands/hosting_service/hosting_service.go +++ b/pkg/commands/hosting_service/hosting_service.go @@ -62,6 +62,18 @@ func (self *HostingServiceMgr) GetCommitURL(commitHash 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 { @@ -73,8 +85,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 } @@ -146,23 +164,44 @@ type ServiceDefinition struct { // can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex 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 } diff --git a/pkg/commands/models/github.go b/pkg/commands/models/github.go new file mode 100644 index 000000000..d938fe1ee --- /dev/null +++ b/pkg/commands/models/github.go @@ -0,0 +1,24 @@ +package models + +// TODO: see if I need to store the head repo name in case it differs from the base repo +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 { + Login string `json:"login"` +} diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 26d10f73a..dce596ee3 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -244,6 +244,8 @@ type GitConfig struct { // When copying commit hashes to the clipboard, truncate them to this // length. Set to 40 to disable truncation. TruncateCopiedCommitHashesTo int `yaml:"truncateCopiedCommitHashesTo"` + // If true and if if `gh` is installed and on version >=2, we will use `gh` to display pull requests against branches. + EnableGithubCli bool `yaml:"enableGithubCli"` } type PagerType string @@ -748,6 +750,7 @@ func GetDefaultConfig() *UserConfig { CommitPrefixes: map[string]CommitPrefixConfig(nil), ParseEmoji: false, TruncateCopiedCommitHashesTo: 12, + EnableGithubCli: true, }, Refresher: RefresherConfig{ RefreshInterval: 10, diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 061502a43..4a64be0e2 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -30,7 +30,8 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() { if userConfig.Git.AutoFetch { fetchInterval := userConfig.Refresher.FetchInterval if fetchInterval > 0 { - go utils.Safe(self.startBackgroundFetch) + refreshInterval := self.gui.UserConfig.Refresher.FetchInterval + go utils.Safe(func() { self.startBackgroundFetch(refreshInterval) }) } else { self.gui.c.Log.Errorf( "Value of config option 'refresher.fetchInterval' (%d) is invalid, disabling auto-fetch", @@ -73,19 +74,15 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() { } } -func (self *BackgroundRoutineMgr) startBackgroundFetch() { +func (self *BackgroundRoutineMgr) startBackgroundFetch(refreshInterval int) { self.gui.waitForIntro.Wait() isNew := self.gui.IsNewRepo - userConfig := self.gui.UserConfig - if !isNew { - time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second) - } err := self.backgroundFetch() if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew { _ = self.gui.c.Alert(self.gui.c.Tr.NoAutomaticGitFetchTitle, self.gui.c.Tr.NoAutomaticGitFetchBody) } else { - self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, func() error { + self.goEvery(time.Second*time.Duration(refreshInterval), self.gui.stopChan, func() error { err := self.backgroundFetch() self.gui.c.Render() return err @@ -129,7 +126,7 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru func (self *BackgroundRoutineMgr) backgroundFetch() (err error) { err = self.gui.git.Sync.FetchBackground() - _ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC}) + _ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS, types.PULL_REQUESTS}, Mode: types.ASYNC}) return err } diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index d289f2729..72008e98a 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -28,6 +28,8 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext { return presentation.GetBranchListDisplayStrings( viewModel.GetItems(), c.State().GetItemOperation, + c.Model().PullRequests, + c.Model().Remotes, c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.Modes().Diffing.Ref, c.Views().Branches.Width(), diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index ba39fef5a..9988ae671 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -69,6 +69,7 @@ func (gui *Gui) resetHelpersAndControllers() { mergeConflictsHelper, worktreeHelper, searchHelper, + suggestionsHelper, ) diffHelper := helpers.NewDiffHelper(helperCommon) cherryPickHelper := helpers.NewCherryPickHelper( diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index 1f1050dc9..9b3a8ab9a 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/ |