diff --git a/docs-master/Config.md b/docs-master/Config.md index 9931fda61fe..6cac01aad8f 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -709,6 +709,7 @@ keybinding: viewPullRequestOptions: O openPullRequestInBrowser: G copyPullRequestURL: + copyBranchURL: "y" checkoutBranchByName: c forceCheckoutBranch: F checkoutPreviousBranch: '-' diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index ca1541dd21d..03f5ba538d0 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -183,6 +183,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` O `` | View create pull request options | | | `` G `` | Open pull request in browser | | | `` `` | Copy pull request URL to clipboard | | +| `` y `` | Copy branch URL to clipboard | | | `` c `` | Checkout by name | Checkout by name. In the input box you can enter '-' to switch to the previous branch. | | `` - `` | Checkout previous branch | | | `` F `` | Force checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index 69479db13e7..fc61ef904d3 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -382,6 +382,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味 | `` O `` | プルリクエスト作成オプションを表示 | | | `` G `` | Open pull request in browser | | | `` `` | プルリクエストURLをクリップボードにコピー | | +| `` y `` | Copy branch URL to clipboard | | | `` c `` | 名前でチェックアウト | 名前でチェックアウトします。入力ボックスに「-」を入力すると、最後のブランチをチェックアウトすることができます。 | | `` - `` | 直前のブランチにチェックアウト | | | `` F `` | 強制チェックアウト | 選択したブランチを強制的にチェックアウトします。これにより、選択したブランチをチェックアウトする前にワーキングディレクトリ内のすべてのローカル変更が破棄されます。 | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index eeb5ed88510..d99ec63f3ca 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -217,6 +217,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` O `` | 풀 리퀘스트 생성 옵션 | | | `` G `` | Open pull request in browser | | | `` `` | 풀 리퀘스트 URL을 클립보드에 복사 | | +| `` y `` | Copy branch URL to clipboard | | | `` c `` | 이름으로 체크아웃 | Checkout by name. In the input box you can enter '-' to switch to the previous branch. | | `` - `` | Checkout previous branch | | | `` F `` | 강제 체크아웃 | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index 21b8c5b4c60..b83e7479331 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -107,6 +107,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` O `` | Bekijk opties voor pull-aanvraag | | | `` G `` | Open pull request in browser | | | `` `` | Kopieer de URL van het pull-verzoek naar het klembord | | +| `` y `` | Copy branch URL to clipboard | | | `` c `` | Uitchecken bij naam | Checkout by name. In the input box you can enter '-' to switch to the previous branch. | | `` - `` | Checkout previous branch | | | `` F `` | Forceer checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index 622a134fd53..0f5d3e142e0 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -149,6 +149,7 @@ _Legenda: `` oznacza ctrl+b, `` oznacza alt+b, `B` oznacza shift+b_ | `` O `` | Zobacz opcje tworzenia pull requesta | | | `` G `` | Open pull request in browser | | | `` `` | Kopiuj adres URL żądania ściągnięcia do schowka | | +| `` y `` | Copy branch URL to clipboard | | | `` c `` | Przełącz według nazwy | Przełącz według nazwy. W polu wprowadzania możesz wpisać '-' aby przełączyć się na ostatnią gałąź. | | `` - `` | Checkout previous branch | | | `` F `` | Wymuś przełączenie | Wymuś przełączenie wybranej gałęzi. To spowoduje odrzucenie wszystkich lokalnych zmian w drzewie roboczym przed przełączeniem na wybraną gałąź. | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index 81dc4085e5f..05d50a026a3 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -99,6 +99,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` O `` | View create pull request options | | | `` G `` | Open pull request in browser | | | `` `` | Copiar URL do pull request para área de transferência | | +| `` y `` | Copy branch URL to clipboard | | | `` c `` | Checar por nome | Checar por nome. Na caixa de entrada você pode inserir '-' para trocar para a última branch | | `` - `` | Checkout da branch anterior | | | `` F `` | Forçar checagem | Forçar checagem da branch selecionada. Isso irá descartar todas as mudanças no seu diretório de trabalho antes cheque a branch selecionada | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index b4531eb7305..1a414f8067f 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -217,6 +217,7 @@ _Связки клавиш_ | `` O `` | Создать параметры запроса принятие изменений | | | `` G `` | Open pull request in browser | | | `` `` | Скопировать URL запроса на принятие изменений в буфер обмена | | +| `` y `` | Copy branch URL to clipboard | | | `` c `` | Переключить по названию | Checkout by name. In the input box you can enter '-' to switch to the previous branch. | | `` - `` | Checkout previous branch | | | `` F `` | Принудительное переключение | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index 0385e486b5c..f0d1a7a8f02 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -232,6 +232,7 @@ _图例:`` 意味着ctrl+b, `意味着Alt+b, `B` 意味着shift+b_ | `` O `` | 创建拉取请求选项 | | | `` G `` | Open pull request in browser | | | `` `` | 复制拉取请求 URL 到剪贴板 | | +| `` y `` | Copy branch URL to clipboard | | | `` c `` | 按名称检出 | 按名称检出。在输入框中,您可以输入'-' 来切换到最后一个分支。 | | `` - `` | 签出上一个分支 | | | `` F `` | 强制检出 | 强制检出所选分支。这将在检出所选分支之前放弃工作目录中的所有本地更改。 | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index c0579e0ced8..932fcbf3ece 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -292,6 +292,7 @@ _說明:`` 表示 Ctrl+B、`` 表示 Alt+B,`B`表示 Shift+B | `` O `` | 建立拉取請求選項 | | | `` G `` | Open pull request in browser | | | `` `` | 複製拉取請求的 URL 到剪貼板 | | +| `` y `` | Copy branch URL to clipboard | | | `` c `` | 根據名稱檢出 | Checkout by name. In the input box you can enter '-' to switch to the previous branch. | | `` - `` | Checkout previous branch | | | `` F `` | 強制檢出 | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. | diff --git a/pkg/commands/hosting_service/definitions.go b/pkg/commands/hosting_service/definitions.go index 130bf04811f..f4f5f25caed 100644 --- a/pkg/commands/hosting_service/definitions.go +++ b/pkg/commands/hosting_service/definitions.go @@ -19,6 +19,7 @@ var githubServiceDef = ServiceDefinition{ pullRequestURLIntoDefaultBranch: "/compare/{{.From}}?expand=1", pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1", commitURL: "/commit/{{.CommitHash}}", + branchURL: "/tree/{{.BranchName}}", regexStrings: defaultUrlRegexStrings, repoURLTemplate: defaultRepoURLTemplate, repoNameTemplate: defaultRepoNameTemplate, @@ -29,6 +30,7 @@ var bitbucketServiceDef = ServiceDefinition{ pullRequestURLIntoDefaultBranch: "/pull-requests/new?source={{.From}}&t=1", pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1", commitURL: "/commits/{{.CommitHash}}", + branchURL: "/branch/{{.BranchName}}", regexStrings: []string{ `^(?:https?|ssh)://.*/(?P.*)/(?P.*?)(?:\.git)?$`, `^.*@.*:/*(?P.*)/(?P.*?)(?:\.git)?$`, @@ -42,6 +44,7 @@ var gitLabServiceDef = ServiceDefinition{ pullRequestURLIntoDefaultBranch: "/-/merge_requests/new?merge_request%5Bsource_branch%5D={{.From}}", pullRequestURLIntoTargetBranch: "/-/merge_requests/new?merge_request%5Bsource_branch%5D={{.From}}&merge_request%5Btarget_branch%5D={{.To}}", commitURL: "/-/commit/{{.CommitHash}}", + branchURL: "/-/tree/{{.BranchName}}", regexStrings: defaultUrlRegexStrings, repoURLTemplate: defaultRepoURLTemplate, repoNameTemplate: defaultRepoNameTemplate, @@ -52,6 +55,7 @@ var azdoServiceDef = ServiceDefinition{ pullRequestURLIntoDefaultBranch: "/pullrequestcreate?sourceRef={{.From}}", pullRequestURLIntoTargetBranch: "/pullrequestcreate?sourceRef={{.From}}&targetRef={{.To}}", commitURL: "/commit/{{.CommitHash}}", + branchURL: "?version=GB{{.BranchName}}", regexStrings: []string{ `^.+@vs-ssh\.visualstudio\.com[:/](?:v3/)?(?P[^/]+)/(?P[^/]+)/(?P[^/]+?)(?:\.git)?$`, `^git@ssh.dev.azure.com.*/(?P.*)/(?P.*)/(?P.*?)(?:\.git)?$`, @@ -67,6 +71,7 @@ var bitbucketServerServiceDef = ServiceDefinition{ pullRequestURLIntoDefaultBranch: "/pull-requests?create&sourceBranch={{.From}}", pullRequestURLIntoTargetBranch: "/pull-requests?create&targetBranch={{.To}}&sourceBranch={{.From}}", commitURL: "/commits/{{.CommitHash}}", + branchURL: "/browse?at={{.BranchName}}", regexStrings: []string{ `^ssh://git@.*/(?P.*)/(?P.*?)(?:\.git)?$`, `^https://.*/scm/(?P.*)/(?P.*?)(?:\.git)?$`, @@ -80,6 +85,7 @@ var giteaServiceDef = ServiceDefinition{ pullRequestURLIntoDefaultBranch: "/compare/{{.From}}", pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}", commitURL: "/commit/{{.CommitHash}}", + branchURL: "/src/branch/{{.BranchName}}", regexStrings: defaultUrlRegexStrings, repoURLTemplate: defaultRepoURLTemplate, } @@ -89,6 +95,7 @@ var codebergServiceDef = ServiceDefinition{ pullRequestURLIntoDefaultBranch: "/compare/{{.From}}", pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}", commitURL: "/commit/{{.CommitHash}}", + branchURL: "/src/branch/{{.BranchName}}", regexStrings: defaultUrlRegexStrings, repoURLTemplate: defaultRepoURLTemplate, } diff --git a/pkg/commands/hosting_service/hosting_service.go b/pkg/commands/hosting_service/hosting_service.go index 620d0d0a788..c3567880fdb 100644 --- a/pkg/commands/hosting_service/hosting_service.go +++ b/pkg/commands/hosting_service/hosting_service.go @@ -73,6 +73,15 @@ func (self *HostingServiceMgr) GetRepoName() (string, error) { return repoName, nil } +func (self *HostingServiceMgr) GetBranchURL(branchName string) (string, error) { + gitService, err := self.getService() + if err != nil { + return "", err + } + + return gitService.getBranchURL(url.QueryEscape(branchName)), nil +} + func (self *HostingServiceMgr) getService() (*Service, error) { serviceDomain, err := self.getServiceDomain(self.remoteURL) if err != nil { @@ -159,6 +168,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 @@ -239,6 +249,10 @@ func (self *Service) getCommitURL(commitHash string) string { return self.resolveUrl(self.commitURL, map[string]string{"CommitHash": commitHash}) } +func (self *Service) getBranchURL(branchName string) string { + return self.resolveUrl(self.branchURL, map[string]string{"BranchName": branchName}) +} + 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 c2fabcd0de6..e25929173fd 100644 --- a/pkg/commands/hosting_service/hosting_service_test.go +++ b/pkg/commands/hosting_service/hosting_service_test.go @@ -577,3 +577,96 @@ func TestGetPullRequestURL(t *testing.T) { }) } } + +func TestGetBranchURL(t *testing.T) { + type scenario struct { + testName string + branchName string + remoteUrl string + configServiceDomains map[string]string + test func(url string, err error) + } + + scenarios := []scenario{ + { + testName: "Returns branch URL for github (SSH)", + branchName: "feature/my-feature", + remoteUrl: "git@github.com:peter/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://github.com/peter/calculator/tree/feature%2Fmy-feature", url) + }, + }, + { + testName: "Returns branch URL for github (HTTPS)", + branchName: "feature/my-feature", + remoteUrl: "https://github.com/peter/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://github.com/peter/calculator/tree/feature%2Fmy-feature", url) + }, + }, + { + testName: "Returns branch URL for gitlab", + branchName: "feature/ui", + remoteUrl: "git@gitlab.com:peter/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://gitlab.com/peter/calculator/-/tree/feature%2Fui", url) + }, + }, + { + testName: "Returns branch URL for bitbucket", + branchName: "feature/profile-page", + remoteUrl: "git@bitbucket.org:johndoe/social_network.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://bitbucket.org/johndoe/social_network/branch/feature%2Fprofile-page", url) + }, + }, + { + testName: "Returns branch URL for gitea", + branchName: "main", + remoteUrl: "git@try.gitea.io:johndoe/myrepo.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://try.gitea.io/johndoe/myrepo/src/branch/main", url) + }, + }, + { + testName: "Returns branch URL for codeberg", + branchName: "develop", + remoteUrl: "git@codeberg.org:johndoe/myrepo.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://codeberg.org/johndoe/myrepo/src/branch/develop", url) + }, + }, + { + testName: "Escapes reserved URL characters in branch name", + branchName: "feature/issue#42", + remoteUrl: "git@github.com:peter/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://github.com/peter/calculator/tree/feature%2Fissue%2342", url) + }, + }, + { + testName: "Returns error for unsupported service", + branchName: "main", + remoteUrl: "git@unknown-host.com:peter/calculator.git", + test: func(url string, err error) { + assert.Error(t, err) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + tr := i18n.EnglishTranslationSet() + log := &fakes.FakeFieldLogger{} + hostingServiceMgr := NewHostingServiceMgr(log, tr, s.remoteUrl, s.configServiceDomains) + s.test(hostingServiceMgr.GetBranchURL(s.branchName)) + }) + } +} diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 29778d63928..4ac6cc8da14 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -535,6 +535,7 @@ type KeybindingBranchesConfig struct { ViewPullRequestOptions string `yaml:"viewPullRequestOptions"` OpenPullRequestInBrowser string `yaml:"openPullRequestInBrowser"` CopyPullRequestURL string `yaml:"copyPullRequestURL"` + CopyBranchURL string `yaml:"copyBranchURL"` CheckoutBranchByName string `yaml:"checkoutBranchByName"` ForceCheckoutBranch string `yaml:"forceCheckoutBranch"` CheckoutPreviousBranch string `yaml:"checkoutPreviousBranch"` @@ -1005,6 +1006,7 @@ func GetDefaultConfig() *UserConfig { CreatePullRequest: "o", ViewPullRequestOptions: "O", OpenPullRequestInBrowser: "G", + CopyBranchURL: "y", CheckoutBranchByName: "c", ForceCheckoutBranch: "F", CheckoutPreviousBranch: "-", diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 1c3d9e35061..cd79c9fbea6 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -93,6 +93,12 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty GetDisabledReason: self.require(self.singleItemSelected()), Description: self.c.Tr.CopyPullRequestURL, }, + { + Key: opts.GetKey(opts.Config.Branches.CopyBranchURL), + Handler: self.copyBranchURL, + GetDisabledReason: self.require(self.singleItemSelected()), + Description: self.c.Tr.CopyBranchURL, + }, { Key: opts.GetKey(opts.Config.Branches.CheckoutBranchByName), Handler: self.checkoutByName, @@ -555,6 +561,29 @@ func (self *BranchesController) copyPullRequestURL() error { return nil } +func (self *BranchesController) copyBranchURL() error { + branch := self.context().GetSelected() + + branchExistsOnRemote := self.c.Git().Remote.CheckRemoteBranchExists(branch.Name) + + if !branchExistsOnRemote { + return errors.New(self.c.Tr.NoBranchOnRemote) + } + + url, err := self.c.Helpers().Host.GetBranchURL(branch.Name) + if err != nil { + return err + } + self.c.LogAction(self.c.Tr.Actions.CopyBranchURL) + if err := self.c.OS().CopyToClipboard(url); err != nil { + return err + } + + self.c.Toast(self.c.Tr.BranchURLCopiedToClipboard) + + return nil +} + func (self *BranchesController) forceCheckout() error { branch := self.context().GetSelected() message := self.c.Tr.SureForceCheckout diff --git a/pkg/gui/controllers/helpers/host_helper.go b/pkg/gui/controllers/helpers/host_helper.go index 42115e86f82..1559ed3e60f 100644 --- a/pkg/gui/controllers/helpers/host_helper.go +++ b/pkg/gui/controllers/helpers/host_helper.go @@ -34,6 +34,14 @@ func (self *HostHelper) GetCommitURL(commitHash string) (string, error) { return mgr.GetCommitURL(commitHash) } +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) { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 002adb11410..d952d752e2f 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -284,6 +284,7 @@ type TranslationSet struct { CopyPullRequestURL string OpenPullRequestInBrowser string NoPullRequestForBranch string + CopyBranchURL string NoBranchOnRemote string Fetch string FetchTooltip string @@ -744,6 +745,7 @@ type TranslationSet struct { SuggestionsSubtitle string ExtrasTitle string PullRequestURLCopiedToClipboard string + BranchURLCopiedToClipboard string CommitDiffCopiedToClipboard string CommitURLCopiedToClipboard string CommitMessageCopiedToClipboard string @@ -1083,6 +1085,7 @@ type Actions struct { Undo string Redo string CopyPullRequestURL string + CopyBranchURL string OpenMergeTool string OpenCommitInBrowser string OpenPullRequest string @@ -1404,6 +1407,7 @@ func EnglishTranslationSet() *TranslationSet { CopyPullRequestURL: `Copy pull request URL to clipboard`, OpenPullRequestInBrowser: `Open pull request in browser`, NoPullRequestForBranch: `No pull request found for this branch`, + CopyBranchURL: `Copy branch URL to clipboard`, NoBranchOnRemote: `This branch doesn't exist on remote. You need to push it to remote first.`, Fetch: `Fetch`, FetchTooltip: "Fetch changes from remote.", @@ -1868,6 +1872,7 @@ func EnglishTranslationSet() *TranslationSet { SuggestionsSubtitle: "(press %s to delete, %s to edit)", ExtrasTitle: "Command log", PullRequestURLCopiedToClipboard: "Pull request URL copied to clipboard", + BranchURLCopiedToClipboard: "Branch URL copied to clipboard", CommitDiffCopiedToClipboard: "Commit diff copied to clipboard", CommitURLCopiedToClipboard: "Commit URL copied to clipboard", CommitMessageCopiedToClipboard: "Commit message copied to clipboard", @@ -2164,6 +2169,7 @@ func EnglishTranslationSet() *TranslationSet { Undo: "Undo", Redo: "Redo", CopyPullRequestURL: "Copy pull request URL", + CopyBranchURL: "Copy branch URL", OpenMergeTool: "Open merge tool", OpenCommitInBrowser: "Open commit in browser", OpenPullRequest: "Open pull request in browser", diff --git a/schema-master/config.json b/schema-master/config.json index c4312981fa2..685fdd7d035 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -876,6 +876,10 @@ "type": "string", "default": "\u003cc-y\u003e" }, + "copyBranchURL": { + "type": "string", + "default": "y" + }, "checkoutBranchByName": { "type": "string", "default": "c"