diff --git a/pkg/commands/git_commands/remote.go b/pkg/commands/git_commands/remote.go index ca361067976..452796c6436 100644 --- a/pkg/commands/git_commands/remote.go +++ b/pkg/commands/git_commands/remote.go @@ -1,7 +1,9 @@ package git_commands import ( + "errors" "fmt" + "path" "strings" "github.com/jesseduffield/gocui" @@ -66,24 +68,76 @@ func (self *RemoteCommands) DeleteRemoteTag(task gocui.Task, remoteName string, return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run() } -// CheckRemoteBranchExists Returns remote branch +// CheckRemoteBranchExists returns a boolean indicating whether or not +// the given branch has an upstream. func (self *RemoteCommands) CheckRemoteBranchExists(branchName string) bool { - cmdArgs := NewGitCmd("show-ref"). - Arg("--verify", "--", fmt.Sprintf("refs/remotes/origin/%s", branchName)). - ToArgv() - - _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() - + _, err := self.getRemoteRef(branchName) return err == nil } // Resolve what might be a aliased URL into a full URL // SEE: `man -P 'less +/--get-url +n' git-ls-remote` -func (self *RemoteCommands) GetRemoteURL(remoteName string) (string, error) { +func (self *RemoteCommands) GetRemoteURL() (string, error) { + remoteName := self.getRemoteName() + if remoteName == "" { + return "", errors.New("could not find upstream remote") + } + cmdArgs := NewGitCmd("ls-remote"). Arg("--get-url", remoteName). ToArgv() - url, err := self.cmd.New(cmdArgs).RunWithOutput() + url, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() return strings.TrimSpace(url), err } + +func (self *RemoteCommands) CommitExistsOnRemote(commitHash string) bool { + return self.findRemoteBranchForCommit(commitHash) != "" +} + +func (self *RemoteCommands) getRemoteRef(branchName string) (string, error) { + cmdArgs := NewGitCmd("rev-parse"). + Arg("--symbolic-full-name", fmt.Sprintf("%s@{upstream}", branchName)). + ToArgv() + + remote, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() + if err != nil && branchName == "" { + // if we couldn't find an upstream and the caller isn't asking about a specific + // branch we'll return the first valid remote we find (if any) + cmdArgs := NewGitCmd("rev-parse"). + Arg("--symbolic-full-name", "--remotes"). + ToArgv() + remote, err = self.cmd.New(cmdArgs).DontLog().RunWithOutput() + remote, _, _ = strings.Cut(remote, "\n") + } + + return remote, err +} + +func (self *RemoteCommands) getRemoteName() string { + ref, err := self.getRemoteRef("") + if err != nil { + return "" + } + // refs/remotes/remote-name/branch + // ^^^^^^^^^^^ + return path.Base(path.Dir(ref)) +} + +func (self *RemoteCommands) findRemoteBranchForCommit(commitHash string) string { + cmdArgs := NewGitCmd("branch"). + Arg("--remotes", "--contains", commitHash). + ToArgv() + + remotes, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() + if err != nil { + return "" + } + + // grab the first returned remote + remote, _, _ := strings.Cut(remotes, "\n") + if remote != "" { + return path.Base(remote) + } + return "" +} diff --git a/pkg/commands/hosting_service/hosting_service.go b/pkg/commands/hosting_service/hosting_service.go index 1c328fd0da2..bd424a1e304 100644 --- a/pkg/commands/hosting_service/hosting_service.go +++ b/pkg/commands/hosting_service/hosting_service.go @@ -26,15 +26,23 @@ type HostingServiceMgr struct { // see https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-pull-request-urls configServiceDomains map[string]string + + commitExistsOnRemote func(string) bool } // NewHostingServiceMgr creates new instance of PullRequest -func NewHostingServiceMgr(log logrus.FieldLogger, tr *i18n.TranslationSet, remoteURL string, configServiceDomains map[string]string) *HostingServiceMgr { +func NewHostingServiceMgr( + log logrus.FieldLogger, + tr *i18n.TranslationSet, remoteURL string, + configServiceDomains map[string]string, + commitExistsOnRemote func(string) bool, +) *HostingServiceMgr { return &HostingServiceMgr{ log: log, tr: tr, remoteURL: remoteURL, configServiceDomains: configServiceDomains, + commitExistsOnRemote: commitExistsOnRemote, } } @@ -56,9 +64,10 @@ func (self *HostingServiceMgr) GetCommitURL(commitHash string) (string, error) { return "", err } - pullRequestURL := gitService.getCommitURL(commitHash) - - return pullRequestURL, nil + if !self.commitExistsOnRemote(commitHash) { + return "", errors.New("no remote URL available") + } + return gitService.getCommitURL(commitHash), nil } func (self *HostingServiceMgr) getService() (*Service, error) { @@ -166,17 +175,17 @@ type Service struct { } func (self *Service) getPullRequestURLIntoDefaultBranch(from string) string { - return self.resolveUrl(self.pullRequestURLIntoDefaultBranch, map[string]string{"From": from}) + return self.resolveURL(self.pullRequestURLIntoDefaultBranch, map[string]string{"From": from}) } func (self *Service) getPullRequestURLIntoTargetBranch(from string, to string) string { - return self.resolveUrl(self.pullRequestURLIntoTargetBranch, map[string]string{"From": from, "To": to}) + return self.resolveURL(self.pullRequestURLIntoTargetBranch, map[string]string{"From": from, "To": to}) } func (self *Service) getCommitURL(commitHash string) string { - return self.resolveUrl(self.commitURL, map[string]string{"CommitHash": commitHash}) + return self.resolveURL(self.commitURL, map[string]string{"CommitHash": commitHash}) } -func (self *Service) resolveUrl(templateString string, args map[string]string) string { +func (self *Service) resolveURL(templateString string, args map[string]string) string { return self.repoURL + utils.ResolvePlaceholderString(templateString, args) } diff --git a/pkg/commands/hosting_service/hosting_service_test.go b/pkg/commands/hosting_service/hosting_service_test.go index 2278d46d268..b532d270ddc 100644 --- a/pkg/commands/hosting_service/hosting_service_test.go +++ b/pkg/commands/hosting_service/hosting_service_test.go @@ -429,7 +429,7 @@ func TestGetPullRequestURL(t *testing.T) { t.Run(s.testName, func(t *testing.T) { tr := i18n.EnglishTranslationSet() log := &fakes.FakeFieldLogger{} - hostingServiceMgr := NewHostingServiceMgr(log, tr, s.remoteUrl, s.configServiceDomains) + hostingServiceMgr := NewHostingServiceMgr(log, tr, s.remoteUrl, s.configServiceDomains, nil) s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to)) log.AssertErrors(t, s.expectedLoggedErrors) }) diff --git a/pkg/gui/controllers/basic_commits_controller.go b/pkg/gui/controllers/basic_commits_controller.go index b3a5bf38c64..031570fa8a7 100644 --- a/pkg/gui/controllers/basic_commits_controller.go +++ b/pkg/gui/controllers/basic_commits_controller.go @@ -161,6 +161,13 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e } } + var commitTagsDisabled *types.DisabledReason + if len(commit.Tags) == 0 { + commitTagsDisabled = &types.DisabledReason{ + Text: self.c.Tr.CommitHasNoTags, + } + } + items := []*types.MenuItem{ { Label: self.c.Tr.CommitHash, @@ -191,7 +198,8 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e Key: 'b', }, { - Label: self.c.Tr.CommitURL, + Label: self.c.Tr.CommitURL, + DisabledReason: self.canCopyCommitURL(commit), OnPress: func() error { return self.copyCommitURLToClipboard(commit) }, @@ -211,22 +219,16 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e }, Key: 'a', }, - } - - commitTagsItem := types.MenuItem{ - Label: self.c.Tr.CommitTags, - OnPress: func() error { - return self.copyCommitTagsToClipboard(commit) + { + Label: self.c.Tr.CommitTags, + DisabledReason: commitTagsDisabled, + OnPress: func() error { + return self.copyCommitTagsToClipboard(commit) + }, + Key: 't', }, - Key: 't', } - if len(commit.Tags) == 0 { - commitTagsItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CommitHasNoTags} - } - - items = append(items, &commitTagsItem) - return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.Actions.CopyCommitAttributeToClipboard, Items: items, @@ -382,6 +384,16 @@ func (self *BasicCommitsController) canCopyCommits(selectedCommits []*models.Com return nil } +func (self *BasicCommitsController) canCopyCommitURL(commit *models.Commit) *types.DisabledReason { + _, err := self.c.Helpers().Host.GetCommitURL(commit.Hash()) + if err != nil { + return &types.DisabledReason{ + Text: self.c.Tr.CommitHasNoURL, + } + } + return nil +} + func (self *BasicCommitsController) handleOldCherryPickKey() error { msg := utils.ResolvePlaceholderString(self.c.Tr.OldCherryPickKeyWarning, map[string]string{ diff --git a/pkg/gui/controllers/helpers/host_helper.go b/pkg/gui/controllers/helpers/host_helper.go index 42115e86f82..c8372495a92 100644 --- a/pkg/gui/controllers/helpers/host_helper.go +++ b/pkg/gui/controllers/helpers/host_helper.go @@ -37,10 +37,17 @@ func (self *HostHelper) GetCommitURL(commitHash string) (string, error) { // getting this on every request rather than storing it in state in case our remoteURL changes // from one invocation to the next. func (self *HostHelper) getHostingServiceMgr() (*hosting_service.HostingServiceMgr, error) { - remoteUrl, err := self.c.Git().Remote.GetRemoteURL("origin") + remoteUrl, err := self.c.Git().Remote.GetRemoteURL() if err != nil { return nil, err } configServices := self.c.UserConfig().Services - return hosting_service.NewHostingServiceMgr(self.c.Log, self.c.Tr, remoteUrl, configServices), nil + hostingServiceMgr := hosting_service.NewHostingServiceMgr( + self.c.Log, + self.c.Tr, + remoteUrl, + configServices, + self.c.Git().Remote.CommitExistsOnRemote, + ) + return hostingServiceMgr, nil } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 6eba8d5ef17..8e527daf3b2 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -713,6 +713,7 @@ type TranslationSet struct { CommitTagsCopiedToClipboard string CommitHasNoTags string CommitHasNoMessageBody string + CommitHasNoURL string PatchCopiedToClipboard string CopiedToClipboard string ErrCannotEditDirectory string @@ -1766,6 +1767,7 @@ func EnglishTranslationSet() *TranslationSet { CommitTagsCopiedToClipboard: "Commit tags copied to clipboard", CommitHasNoTags: "Commit has no tags", CommitHasNoMessageBody: "Commit has no message body", + CommitHasNoURL: "Commit does not exist on remote", PatchCopiedToClipboard: "Patch copied to clipboard", CopiedToClipboard: "copied to clipboard", ErrCannotEditDirectory: "Cannot edit directories: you can only edit individual files",