Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy branch url to clipboard #3343

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion docs/keybindings/Keybindings_en.md
Expand Up @@ -157,7 +157,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` n `` | New branch | |
| `` o `` | Create pull request | |
| `` O `` | View create pull request options | |
| `` <c-y> `` | Copy pull request URL to clipboard | |
| `` y `` | Copy branch attribute to clipboard | |
| `` c `` | Checkout by name | Checkout by name. In the input box you can enter '-' to switch to the last branch. |
| `` F `` | Force checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
Expand Down
2 changes: 1 addition & 1 deletion docs/keybindings/Keybindings_ja.md
Expand Up @@ -227,7 +227,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` n `` | 新しいブランチを作成 | |
| `` o `` | Pull Requestを作成 | |
| `` O `` | View create pull request options | |
| `` <c-y> `` | Pull RequestのURLをクリップボードにコピー | |
| `` y `` | Copy branch attribute to clipboard | |
| `` c `` | Checkout by name | Checkout by name. In the input box you can enter '-' to switch to the last branch. |
| `` F `` | Force checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
Expand Down
2 changes: 1 addition & 1 deletion docs/keybindings/Keybindings_ko.md
Expand Up @@ -183,7 +183,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` n `` | 새 브랜치 생성 | |
| `` o `` | 풀 리퀘스트 생성 | |
| `` O `` | 풀 리퀘스트 생성 옵션 | |
| `` <c-y> `` | 풀 리퀘스트 URL을 클립보드에 복사 | |
| `` y `` | Copy branch attribute to clipboard | |
| `` c `` | 이름으로 체크아웃 | Checkout by name. In the input box you can enter '-' to switch to the last branch. |
| `` F `` | 강제 체크아웃 | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
Expand Down
2 changes: 1 addition & 1 deletion docs/keybindings/Keybindings_nl.md
Expand Up @@ -96,7 +96,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` n `` | Nieuwe branch | |
| `` o `` | Maak een pull-request | |
| `` O `` | Bekijk opties voor pull-aanvraag | |
| `` <c-y> `` | Kopieer de URL van het pull-verzoek naar het klembord | |
| `` y `` | Copy branch attribute to clipboard | |
| `` c `` | Uitchecken bij naam | Checkout by name. In the input box you can enter '-' to switch to the last branch. |
| `` F `` | Forceer checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
Expand Down
2 changes: 1 addition & 1 deletion docs/keybindings/Keybindings_pl.md
Expand Up @@ -111,7 +111,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` n `` | Nowa gałąź | |
| `` o `` | Utwórz żądanie pobrania | |
| `` O `` | Utwórz opcje żądania ściągnięcia | |
| `` <c-y> `` | Skopiuj adres URL żądania pobrania do schowka | |
| `` y `` | Copy branch attribute to clipboard | |
| `` c `` | Przełącz używając nazwy | Checkout by name. In the input box you can enter '-' to switch to the last branch. |
| `` F `` | Wymuś przełączenie | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
Expand Down
2 changes: 1 addition & 1 deletion docs/keybindings/Keybindings_ru.md
Expand Up @@ -183,7 +183,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` n `` | Новая ветка | |
| `` o `` | Создать запрос на принятие изменений | |
| `` O `` | Создать параметры запроса принятие изменений | |
| `` <c-y> `` | Скопировать URL запроса на принятие изменений в буфер обмена | |
| `` y `` | Copy branch attribute to clipboard | |
| `` c `` | Переключить по названию | Checkout by name. In the input box you can enter '-' to switch to the last branch. |
| `` F `` | Принудительное переключение | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
Expand Down
2 changes: 1 addition & 1 deletion docs/keybindings/Keybindings_zh-CN.md
Expand Up @@ -86,7 +86,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` n `` | 新分支 | |
| `` o `` | 创建抓取请求 | |
| `` O `` | 创建抓取请求选项 | |
| `` <c-y> `` | 将抓取请求 URL 复制到剪贴板 | |
| `` y `` | Copy branch attribute to clipboard | |
| `` c `` | 按名称检出 | Checkout by name. In the input box you can enter '-' to switch to the last branch. |
| `` F `` | 强制检出 | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
Expand Down
2 changes: 1 addition & 1 deletion docs/keybindings/Keybindings_zh-TW.md
Expand Up @@ -258,7 +258,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` n `` | 新分支 | |
| `` o `` | 建立拉取請求 | |
| `` O `` | 建立拉取請求選項 | |
| `` <c-y> `` | 複製拉取請求的 URL 到剪貼板 | |
| `` y `` | Copy branch attribute to clipboard | |
| `` c `` | 根據名稱檢出 | Checkout by name. In the input box you can enter '-' to switch to the last branch. |
| `` F `` | 強制檢出 | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
Expand Down
8 changes: 6 additions & 2 deletions pkg/commands/git_commands/remote.go
Expand Up @@ -66,9 +66,13 @@ func (self *RemoteCommands) DeleteRemoteTag(task gocui.Task, remoteName string,
}

// CheckRemoteBranchExists Returns remote branch
func (self *RemoteCommands) CheckRemoteBranchExists(branchName string) bool {
func (self *RemoteCommands) CheckRemoteBranchExists(branchName string, upstreamRemote string) bool {
if upstreamRemote == "" {
upstreamRemote = "origin"
}

cmdArgs := NewGitCmd("show-ref").
Arg("--verify", "--", fmt.Sprintf("refs/remotes/origin/%s", branchName)).
Arg("--verify", "--", fmt.Sprintf("refs/remotes/%s/%s", upstreamRemote, branchName)).
ToArgv()

_, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
Expand Down
6 changes: 6 additions & 0 deletions pkg/commands/hosting_service/definitions.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it these are correct because I've only checked for github.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't verify the branch URL for bitbucket server. The others should be correct.

Expand Up @@ -15,6 +15,7 @@ var githubServiceDef = ServiceDefinition{
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}?expand=1",
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1",
commitURL: "/commit/{{.CommitSha}}",
branchURL: "/tree/{{.BranchName}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
}
Expand All @@ -24,6 +25,7 @@ var bitbucketServiceDef = ServiceDefinition{
pullRequestURLIntoDefaultBranch: "/pull-requests/new?source={{.From}}&t=1",
pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1",
commitURL: "/commits/{{.CommitSha}}",
branchURL: "/src/{{.BranchName}}",
regexStrings: []string{
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^.*@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
Expand All @@ -36,6 +38,7 @@ var gitLabServiceDef = ServiceDefinition{
pullRequestURLIntoDefaultBranch: "/-/merge_requests/new?merge_request[source_branch]={{.From}}",
pullRequestURLIntoTargetBranch: "/-/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}",
commitURL: "/-/commit/{{.CommitSha}}",
branchURL: "/-/tree/{{.BranchName}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
}
Expand All @@ -45,6 +48,7 @@ var azdoServiceDef = ServiceDefinition{
pullRequestURLIntoDefaultBranch: "/pullrequestcreate?sourceRef={{.From}}",
pullRequestURLIntoTargetBranch: "/pullrequestcreate?sourceRef={{.From}}&targetRef={{.To}}",
commitURL: "/commit/{{.CommitSha}}",
branchURL: "?version=GB{{.BranchName}}",
regexStrings: []string{
`^[email protected].*/(?P<org>.*)/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^https://.*@dev.azure.com/(?P<org>.*?)/(?P<project>.*?)/_git/(?P<repo>.*?)(?:\.git)?$`,
Expand All @@ -57,6 +61,7 @@ var bitbucketServerServiceDef = ServiceDefinition{
pullRequestURLIntoDefaultBranch: "/pull-requests?create&sourceBranch={{.From}}",
pullRequestURLIntoTargetBranch: "/pull-requests?create&targetBranch={{.To}}&sourceBranch={{.From}}",
commitURL: "/commits/{{.CommitSha}}",
branchURL: "/browse?at=refs/heads/{{.BranchName}}",
regexStrings: []string{
`^ssh://git@.*/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^https://.*/scm/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
Expand All @@ -69,6 +74,7 @@ var giteaServiceDef = ServiceDefinition{
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}",
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}",
commitURL: "/commit/{{.CommitSha}}",
branchURL: "/src/branch/{{.BranchName}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
}
Expand Down
16 changes: 16 additions & 0 deletions pkg/commands/hosting_service/hosting_service.go
Expand Up @@ -62,6 +62,17 @@ func (self *HostingServiceMgr) GetCommitURL(commitSha string) (string, error) {
return pullRequestURL, nil
}

func (self *HostingServiceMgr) GetBranchURL(branchName string) (string, error) {
gitService, err := self.getService()
if err != nil {
return "", err
}

branchURL := gitService.getBranchURL(branchName)

return branchURL, nil
}

func (self *HostingServiceMgr) getService() (*Service, error) {
serviceDomain, err := self.getServiceDomain(self.remoteURL)
if err != nil {
Expand Down Expand Up @@ -142,6 +153,7 @@ type ServiceDefinition struct {
pullRequestURLIntoDefaultBranch string
pullRequestURLIntoTargetBranch string
commitURL string
branchURL string
regexStrings []string

// can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex
Expand Down Expand Up @@ -178,6 +190,10 @@ func (self *Service) getCommitURL(commitSha string) string {
return self.resolveUrl(self.commitURL, map[string]string{"CommitSha": commitSha})
}

func (self *Service) getBranchURL(branch string) string {
return self.resolveUrl(self.branchURL, map[string]string{"BranchName": branch})
}

func (self *Service) resolveUrl(templateString string, args map[string]string) string {
return self.repoURL + utils.ResolvePlaceholderString(templateString, args)
}
60 changes: 30 additions & 30 deletions pkg/config/user_config.go
Expand Up @@ -388,21 +388,21 @@ type KeybindingFilesConfig struct {
}

type KeybindingBranchesConfig struct {
CreatePullRequest string `yaml:"createPullRequest"`
ViewPullRequestOptions string `yaml:"viewPullRequestOptions"`
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
RebaseBranch string `yaml:"rebaseBranch"`
RenameBranch string `yaml:"renameBranch"`
MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
ViewGitFlowOptions string `yaml:"viewGitFlowOptions"`
FastForward string `yaml:"fastForward"`
CreateTag string `yaml:"createTag"`
PushTag string `yaml:"pushTag"`
SetUpstream string `yaml:"setUpstream"`
FetchRemote string `yaml:"fetchRemote"`
SortOrder string `yaml:"sortOrder"`
CreatePullRequest string `yaml:"createPullRequest"`
ViewPullRequestOptions string `yaml:"viewPullRequestOptions"`
CopyBranchAttributeToClipboard string `yaml:"copyBranchAttributeToClipboard"`
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
RebaseBranch string `yaml:"rebaseBranch"`
RenameBranch string `yaml:"renameBranch"`
MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
ViewGitFlowOptions string `yaml:"viewGitFlowOptions"`
FastForward string `yaml:"fastForward"`
CreateTag string `yaml:"createTag"`
PushTag string `yaml:"pushTag"`
SetUpstream string `yaml:"setUpstream"`
FetchRemote string `yaml:"fetchRemote"`
SortOrder string `yaml:"sortOrder"`
}

type KeybindingWorktreesConfig struct {
Expand Down Expand Up @@ -785,21 +785,21 @@ func GetDefaultConfig() *UserConfig {
CopyFileInfoToClipboard: "y",
},
Branches: KeybindingBranchesConfig{
CopyPullRequestURL: "<c-y>",
CreatePullRequest: "o",
ViewPullRequestOptions: "O",
CheckoutBranchByName: "c",
ForceCheckoutBranch: "F",
RebaseBranch: "r",
RenameBranch: "R",
MergeIntoCurrentBranch: "M",
ViewGitFlowOptions: "i",
FastForward: "f",
CreateTag: "T",
PushTag: "P",
SetUpstream: "u",
FetchRemote: "f",
SortOrder: "s",
CopyBranchAttributeToClipboard: "y",
CreatePullRequest: "o",
ViewPullRequestOptions: "O",
CheckoutBranchByName: "c",
ForceCheckoutBranch: "F",
RebaseBranch: "r",
RenameBranch: "R",
MergeIntoCurrentBranch: "M",
ViewGitFlowOptions: "i",
FastForward: "f",
CreateTag: "T",
PushTag: "P",
SetUpstream: "u",
FetchRemote: "f",
SortOrder: "s",
},
Worktrees: KeybindingWorktreesConfig{
ViewWorktreeOptions: "w",
Expand Down
83 changes: 70 additions & 13 deletions pkg/gui/controllers/branches_controller.go
@@ -1,7 +1,6 @@
package controllers

import (
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -72,10 +71,10 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Branches.CopyPullRequestURL),
Handler: self.copyPullRequestURL,
Key: opts.GetKey(opts.Config.Branches.CopyBranchAttributeToClipboard),
Handler: self.withItem(self.copyBranchAttributeToClipboard),
GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.CopyPullRequestURL,
Description: self.c.Tr.CopyBranchAttributeToClipboard,
},
{
Key: opts.GetKey(opts.Config.Branches.CheckoutBranchByName),
Expand Down Expand Up @@ -389,15 +388,7 @@ func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *mode
return self.createPullRequestMenu(selectedBranch, checkedOutBranch)
}

func (self *BranchesController) copyPullRequestURL() error {
branch := self.context().GetSelected()

branchExistsOnRemote := self.c.Git().Remote.CheckRemoteBranchExists(branch.Name)

if !branchExistsOnRemote {
return self.c.Error(errors.New(self.c.Tr.NoBranchOnRemote))
}

func (self *BranchesController) copyPullRequestURL(branch *models.Branch) error {
url, err := self.c.Helpers().Host.GetPullRequestURL(branch.Name, "")
if err != nil {
return self.c.Error(err)
Expand All @@ -412,6 +403,62 @@ func (self *BranchesController) copyPullRequestURL() error {
return nil
}

func (self *BranchesController) copyBranchAttributeToClipboard(branch *models.Branch) error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Actions.CopyToClipboard,
Items: []*types.MenuItem{
mark2185 marked this conversation as resolved.
Show resolved Hide resolved
{
Label: self.c.Tr.BranchName,
OnPress: func() error {
return self.copyBranchName(branch)
},
},
{
Label: self.c.Tr.BranchURL,
OnPress: func() error {
return self.copyBranchURL(branch)
},
Key: 'u',
DisabledReason: self.require(self.singleItemSelected(self.remoteBranchExists))(),
},
{
Label: self.c.Tr.PullRequestURL,
OnPress: func() error {
return self.copyPullRequestURL(branch)
},
Key: 'p',
DisabledReason: self.require(self.singleItemSelected(self.remoteBranchExists))(),
},
},
})
}

func (self *BranchesController) copyBranchURL(branch *models.Branch) error {
url, err := self.c.Helpers().Host.GetBranchURL(branch.Name)
if err != nil {
return self.c.Error(err)
}
self.c.LogAction(self.c.Tr.Actions.CopyBranchURL)
if err := self.c.OS().CopyToClipboard(url); err != nil {
return self.c.Error(err)
}

self.c.Toast(self.c.Tr.BranchURLCopiedToClipboard)

return nil
}

func (self *BranchesController) copyBranchName(branch *models.Branch) error {
self.c.LogAction(self.c.Tr.Actions.CopyBranchName)
if err := self.c.OS().CopyToClipboard(branch.Name); err != nil {
return self.c.Error(err)
}

self.c.Toast(self.c.Tr.BranchNameCopiedToClipboard)

return nil
}

func (self *BranchesController) forceCheckout() error {
branch := self.context().GetSelected()
message := self.c.Tr.SureForceCheckout
Expand Down Expand Up @@ -798,3 +845,13 @@ func (self *BranchesController) branchIsReal(branch *models.Branch) *types.Disab

return nil
}

func (self *BranchesController) remoteBranchExists(branch *models.Branch) *types.DisabledReason {
branchExistsOnRemote := self.c.Git().Remote.CheckRemoteBranchExists(branch.Name, branch.UpstreamRemote)

if !branchExistsOnRemote {
return &types.DisabledReason{Text: self.c.Tr.NoBranchOnRemote}
}

return nil
}
9 changes: 9 additions & 0 deletions pkg/gui/controllers/helpers/host_helper.go
Expand Up @@ -9,6 +9,7 @@ import (
type IHostHelper interface {
GetPullRequestURL(from string, to string) (string, error)
GetCommitURL(commitSha string) (string, error)
GetBranchURL(branchName string) (string, error)
}

type HostHelper struct {
Expand Down Expand Up @@ -39,6 +40,14 @@ func (self *HostHelper) GetCommitURL(commitSha string) (string, error) {
return mgr.GetCommitURL(commitSha)
}

func (self *HostHelper) GetBranchURL(branchName string) (string, error) {
mgr, err := self.getHostingServiceMgr()
if err != nil {
return "", err
}
return mgr.GetBranchURL(branchName)
}

// 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) {
Expand Down