| 
 | 1 | +package git_commands  | 
 | 2 | + | 
 | 3 | +import (  | 
 | 4 | +	"encoding/json"  | 
 | 5 | +	"regexp"  | 
 | 6 | +	"strconv"  | 
 | 7 | +	"strings"  | 
 | 8 | + | 
 | 9 | +	"github.com/jesseduffield/lazygit/pkg/commands/models"  | 
 | 10 | +)  | 
 | 11 | + | 
 | 12 | +type GitHubCommands struct {  | 
 | 13 | +	*GitCommon  | 
 | 14 | +}  | 
 | 15 | + | 
 | 16 | +func NewGitHubCommand(gitCommon *GitCommon) *GitHubCommands {  | 
 | 17 | +	return &GitHubCommands{  | 
 | 18 | +		GitCommon: gitCommon,  | 
 | 19 | +	}  | 
 | 20 | +}  | 
 | 21 | + | 
 | 22 | +// https://github.com/cli/cli/issues/2300  | 
 | 23 | +func (self *GitHubCommands) BaseRepo() error {  | 
 | 24 | +	cmdArgs := NewGitCmd("config").  | 
 | 25 | +		Arg("--local", "--get-regexp", ".gh-resolved").  | 
 | 26 | +		ToArgv()  | 
 | 27 | + | 
 | 28 | +	return self.cmd.New(cmdArgs).DontLog().Run()  | 
 | 29 | +}  | 
 | 30 | + | 
 | 31 | +// Ex: git config --local --add "remote.origin.gh-resolved" "jesseduffield/lazygit"  | 
 | 32 | +func (self *GitHubCommands) SetBaseRepo(repository string) (string, error) {  | 
 | 33 | +	cmdArgs := NewGitCmd("config").  | 
 | 34 | +		Arg("--local", "--add", "remote.origin.gh-resolved", repository).  | 
 | 35 | +		ToArgv()  | 
 | 36 | + | 
 | 37 | +	return self.cmd.New(cmdArgs).DontLog().RunWithOutput()  | 
 | 38 | +}  | 
 | 39 | + | 
 | 40 | +func (self *GitHubCommands) FetchRecentPRs() ([]*models.GithubPullRequest, error) {  | 
 | 41 | +	commandOutput, err := self.prList()  | 
 | 42 | +	if err != nil {  | 
 | 43 | +		return nil, err  | 
 | 44 | +	}  | 
 | 45 | + | 
 | 46 | +	prs := []*models.GithubPullRequest{}  | 
 | 47 | +	err = json.Unmarshal([]byte(commandOutput), &prs)  | 
 | 48 | +	if err != nil {  | 
 | 49 | +		return nil, err  | 
 | 50 | +	}  | 
 | 51 | + | 
 | 52 | +	return prs, nil  | 
 | 53 | +}  | 
 | 54 | + | 
 | 55 | +func (self *GitHubCommands) prList() (string, error) {  | 
 | 56 | +	cmdArgs := []string{"gh", "pr", "list", "--limit", "500", "--state", "all", "--json", "state,url,number,headRefName,headRepositoryOwner"}  | 
 | 57 | + | 
 | 58 | +	return self.cmd.New(cmdArgs).DontLog().RunWithOutput()  | 
 | 59 | +}  | 
 | 60 | + | 
 | 61 | +// returns a map from branch name to pull request  | 
 | 62 | +func GenerateGithubPullRequestMap(  | 
 | 63 | +	prs []*models.GithubPullRequest,  | 
 | 64 | +	branches []*models.Branch,  | 
 | 65 | +	remotes []*models.Remote,  | 
 | 66 | +) map[string]*models.GithubPullRequest {  | 
 | 67 | +	res := map[string]*models.GithubPullRequest{}  | 
 | 68 | + | 
 | 69 | +	if len(prs) == 0 {  | 
 | 70 | +		return res  | 
 | 71 | +	}  | 
 | 72 | + | 
 | 73 | +	remotesToOwnersMap := getRemotesToOwnersMap(remotes)  | 
 | 74 | + | 
 | 75 | +	if len(remotesToOwnersMap) == 0 {  | 
 | 76 | +		return res  | 
 | 77 | +	}  | 
 | 78 | + | 
 | 79 | +	// A PR can be identified by two things: the owner e.g. 'jesseduffield' and the  | 
 | 80 | +	// branch name e.g. 'feature/my-feature'. The owner might be different  | 
 | 81 | +	// to the owner of the repo if the PR is from a fork of that repo.  | 
 | 82 | +	type prKey struct {  | 
 | 83 | +		owner      string  | 
 | 84 | +		branchName string  | 
 | 85 | +	}  | 
 | 86 | + | 
 | 87 | +	prByKey := map[prKey]models.GithubPullRequest{}  | 
 | 88 | + | 
 | 89 | +	for _, pr := range prs {  | 
 | 90 | +		prByKey[prKey{owner: pr.UserName(), branchName: pr.BranchName()}] = *pr  | 
 | 91 | +	}  | 
 | 92 | + | 
 | 93 | +	for _, branch := range branches {  | 
 | 94 | +		if !branch.IsTrackingRemote() {  | 
 | 95 | +			continue  | 
 | 96 | +		}  | 
 | 97 | + | 
 | 98 | +		// TODO: support branches whose UpstreamRemote contains a full git  | 
 | 99 | +		// URL rather than just a remote name.  | 
 | 100 | +		owner, foundRemoteOwner := remotesToOwnersMap[branch.UpstreamRemote]  | 
 | 101 | +		if !foundRemoteOwner {  | 
 | 102 | +			continue  | 
 | 103 | +		}  | 
 | 104 | + | 
 | 105 | +		pr, hasPr := prByKey[prKey{owner: owner, branchName: branch.UpstreamBranch}]  | 
 | 106 | + | 
 | 107 | +		if !hasPr {  | 
 | 108 | +			continue  | 
 | 109 | +		}  | 
 | 110 | + | 
 | 111 | +		res[branch.Name] = &pr  | 
 | 112 | +	}  | 
 | 113 | + | 
 | 114 | +	return res  | 
 | 115 | +}  | 
 | 116 | + | 
 | 117 | +func getRemotesToOwnersMap(remotes []*models.Remote) map[string]string {  | 
 | 118 | +	res := map[string]string{}  | 
 | 119 | +	for _, remote := range remotes {  | 
 | 120 | +		if len(remote.Urls) == 0 {  | 
 | 121 | +			continue  | 
 | 122 | +		}  | 
 | 123 | + | 
 | 124 | +		res[remote.Name] = getRepoInfoFromURL(remote.Urls[0]).Owner  | 
 | 125 | +	}  | 
 | 126 | +	return res  | 
 | 127 | +}  | 
 | 128 | + | 
 | 129 | +type RepoInformation struct {  | 
 | 130 | +	Owner      string  | 
 | 131 | +	Repository string  | 
 | 132 | +}  | 
 | 133 | + | 
 | 134 | +// TODO: move this into hosting_service.go  | 
 | 135 | +func getRepoInfoFromURL(url string) RepoInformation {  | 
 | 136 | +	isHTTP := strings.HasPrefix(url, "http")  | 
 | 137 | + | 
 | 138 | +	if isHTTP {  | 
 | 139 | +		splits := strings.Split(url, "/")  | 
 | 140 | +		owner := strings.Join(splits[3:len(splits)-1], "/")  | 
 | 141 | +		repo := strings.TrimSuffix(splits[len(splits)-1], ".git")  | 
 | 142 | + | 
 | 143 | +		return RepoInformation{  | 
 | 144 | +			Owner:      owner,  | 
 | 145 | +			Repository: repo,  | 
 | 146 | +		}  | 
 | 147 | +	}  | 
 | 148 | + | 
 | 149 | +	tmpSplit := strings.Split(url, ":")  | 
 | 150 | +	splits := strings.Split(tmpSplit[1], "/")  | 
 | 151 | +	owner := strings.Join(splits[0:len(splits)-1], "/")  | 
 | 152 | +	repo := strings.TrimSuffix(splits[len(splits)-1], ".git")  | 
 | 153 | + | 
 | 154 | +	return RepoInformation{  | 
 | 155 | +		Owner:      owner,  | 
 | 156 | +		Repository: repo,  | 
 | 157 | +	}  | 
 | 158 | +}  | 
 | 159 | + | 
 | 160 | +// return <installed>, <valid version>  | 
 | 161 | +func (self *GitHubCommands) DetermineGitHubCliState() (bool, bool) {  | 
 | 162 | +	output, err := self.cmd.New([]string{"gh", "--version"}).DontLog().RunWithOutput()  | 
 | 163 | +	if err != nil {  | 
 | 164 | +		// assuming a failure here means that it's not installed  | 
 | 165 | +		return false, false  | 
 | 166 | +	}  | 
 | 167 | + | 
 | 168 | +	if !isGhVersionValid(output) {  | 
 | 169 | +		return true, false  | 
 | 170 | +	}  | 
 | 171 | + | 
 | 172 | +	return true, true  | 
 | 173 | +}  | 
 | 174 | + | 
 | 175 | +func isGhVersionValid(versionStr string) bool {  | 
 | 176 | +	// output should be something like:  | 
 | 177 | +	// gh version 2.0.0 (2021-08-23)  | 
 | 178 | +	// https://github.com/cli/cli/releases/tag/v2.0.0  | 
 | 179 | +	re := regexp.MustCompile(`[^\d]+([\d\.]+)`)  | 
 | 180 | +	matches := re.FindStringSubmatch(versionStr)  | 
 | 181 | + | 
 | 182 | +	if len(matches) == 0 {  | 
 | 183 | +		return false  | 
 | 184 | +	}  | 
 | 185 | + | 
 | 186 | +	ghVersion := matches[1]  | 
 | 187 | +	majorVersion, err := strconv.Atoi(ghVersion[0:1])  | 
 | 188 | +	if err != nil {  | 
 | 189 | +		return false  | 
 | 190 | +	}  | 
 | 191 | +	if majorVersion < 2 {  | 
 | 192 | +		return false  | 
 | 193 | +	}  | 
 | 194 | + | 
 | 195 | +	return true  | 
 | 196 | +}  | 
 | 197 | + | 
 | 198 | +func (self *GitHubCommands) InGithubRepo() bool {  | 
 | 199 | +	remotes, err := self.repo.Remotes()  | 
 | 200 | +	if err != nil {  | 
 | 201 | +		self.Log.Error(err)  | 
 | 202 | +		return false  | 
 | 203 | +	}  | 
 | 204 | + | 
 | 205 | +	if len(remotes) == 0 {  | 
 | 206 | +		return false  | 
 | 207 | +	}  | 
 | 208 | + | 
 | 209 | +	firstRemote := remotes[0]  | 
 | 210 | +	if len(firstRemote.Config().URLs) == 0 {  | 
 | 211 | +		return false  | 
 | 212 | +	}  | 
 | 213 | + | 
 | 214 | +	url := firstRemote.Config().URLs[0]  | 
 | 215 | +	return strings.Contains(url, "github.com")  | 
 | 216 | +}  | 
0 commit comments