mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 23:56:24 -04:00
Add 'copy branch URL to clipboard' feature
Adds a keybinding (default: 'y') in the branches panel to copy the branch's URL on the hosting service to the clipboard. Each supported hosting service has a dedicated URL template: - GitHub: /tree/<branch> - GitLab: /-/tree/<branch> - Bitbucket: /branch/<branch> - Azure DevOps: ?version=GB<branch> - Bitbucket Server: /browse?at=<branch> - Gitea/Codeberg: /src/branch/<branch> Closes #1959
This commit is contained in:
parent
e59c1d1cb7
commit
990db1f676
|
|
@ -21,6 +21,7 @@ var githubServiceDef = ServiceDefinition{
|
|||
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}?expand=1",
|
||||
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1",
|
||||
commitURL: "/commit/{{.CommitHash}}",
|
||||
branchURL: "/tree/{{.BranchName}}",
|
||||
urlRegexps: defaultUrlRegexps,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
repoNameTemplate: defaultRepoNameTemplate,
|
||||
|
|
@ -31,6 +32,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}}",
|
||||
urlRegexps: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`),
|
||||
regexp.MustCompile(`^.*@.*:/*(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`),
|
||||
|
|
@ -44,6 +46,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}}",
|
||||
urlRegexps: defaultUrlRegexps,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
repoNameTemplate: defaultRepoNameTemplate,
|
||||
|
|
@ -54,6 +57,7 @@ var azdoServiceDef = ServiceDefinition{
|
|||
pullRequestURLIntoDefaultBranch: "/pullrequestcreate?sourceRef={{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/pullrequestcreate?sourceRef={{.From}}&targetRef={{.To}}",
|
||||
commitURL: "/commit/{{.CommitHash}}",
|
||||
branchURL: "?version=GB{{.BranchName}}",
|
||||
urlRegexps: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^.+@vs-ssh\.visualstudio\.com[:/](?:v3/)?(?P<org>[^/]+)/(?P<project>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?$`),
|
||||
regexp.MustCompile(`^git@ssh.dev.azure.com.*/(?P<org>.*)/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`),
|
||||
|
|
@ -69,6 +73,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}}",
|
||||
urlRegexps: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^ssh://git@.*/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`),
|
||||
regexp.MustCompile(`^https://.*/scm/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`),
|
||||
|
|
@ -82,6 +87,7 @@ var giteaServiceDef = ServiceDefinition{
|
|||
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}",
|
||||
commitURL: "/commit/{{.CommitHash}}",
|
||||
branchURL: "/src/branch/{{.BranchName}}",
|
||||
urlRegexps: defaultUrlRegexps,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
|
|
@ -91,6 +97,7 @@ var codebergServiceDef = ServiceDefinition{
|
|||
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}",
|
||||
commitURL: "/commit/{{.CommitHash}}",
|
||||
branchURL: "/src/branch/{{.BranchName}}",
|
||||
urlRegexps: defaultUrlRegexps,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,15 @@ func (self *HostingServiceMgr) GetServiceInfo() (ServiceInfo, error) {
|
|||
}, 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 {
|
||||
|
|
@ -196,6 +205,7 @@ type ServiceDefinition struct {
|
|||
pullRequestURLIntoTargetBranch string
|
||||
commitURL string
|
||||
urlRegexps []*regexp.Regexp
|
||||
branchURL string
|
||||
|
||||
// can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex
|
||||
repoURLTemplate string
|
||||
|
|
@ -273,6 +283,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -681,3 +681,96 @@ func TestGetServiceInfo(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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -590,6 +590,7 @@ type KeybindingBranchesConfig struct {
|
|||
ViewPullRequestOptions Keybinding `yaml:"viewPullRequestOptions"`
|
||||
OpenPullRequestInBrowser Keybinding `yaml:"openPullRequestInBrowser"`
|
||||
CopyPullRequestURL Keybinding `yaml:"copyPullRequestURL"`
|
||||
CopyBranchURL Keybinding `yaml:"copyBranchURL"`
|
||||
CheckoutBranchByName Keybinding `yaml:"checkoutBranchByName"`
|
||||
ForceCheckoutBranch Keybinding `yaml:"forceCheckoutBranch"`
|
||||
CheckoutPreviousBranch Keybinding `yaml:"checkoutPreviousBranch"`
|
||||
|
|
@ -1105,6 +1106,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
|||
CreatePullRequest: Keybinding{"o"},
|
||||
ViewPullRequestOptions: Keybinding{"O"},
|
||||
OpenPullRequestInBrowser: Keybinding{"G"},
|
||||
CopyBranchURL: Keybinding{"y"},
|
||||
CheckoutBranchByName: Keybinding{"c"},
|
||||
ForceCheckoutBranch: Keybinding{"F"},
|
||||
CheckoutPreviousBranch: Keybinding{"-"},
|
||||
|
|
|
|||
|
|
@ -100,6 +100,12 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
|||
GetDisabledReason: self.require(self.singleItemSelected()),
|
||||
Description: self.c.Tr.CopyPullRequestURL,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Branches.CopyBranchURL),
|
||||
Handler: self.copyBranchURL,
|
||||
GetDisabledReason: self.require(self.singleItemSelected()),
|
||||
Description: self.c.Tr.CopyBranchURL,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Branches.CheckoutBranchByName),
|
||||
Handler: self.checkoutByName,
|
||||
|
|
@ -533,6 +539,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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ type TranslationSet struct {
|
|||
CopyPullRequestURL string
|
||||
OpenPullRequestInBrowser string
|
||||
NoPullRequestForBranch string
|
||||
CopyBranchURL string
|
||||
NoBranchOnRemote string
|
||||
Fetch string
|
||||
FetchTooltip string
|
||||
|
|
@ -752,6 +753,7 @@ type TranslationSet struct {
|
|||
SuggestionsSubtitle string
|
||||
ExtrasTitle string
|
||||
PullRequestURLCopiedToClipboard string
|
||||
BranchURLCopiedToClipboard string
|
||||
CommitDiffCopiedToClipboard string
|
||||
CommitURLCopiedToClipboard string
|
||||
CommitMessageCopiedToClipboard string
|
||||
|
|
@ -1111,6 +1113,7 @@ type Actions struct {
|
|||
Undo string
|
||||
Redo string
|
||||
CopyPullRequestURL string
|
||||
CopyBranchURL string
|
||||
OpenMergeTool string
|
||||
OpenCommitInBrowser string
|
||||
OpenPullRequest string
|
||||
|
|
@ -1435,6 +1438,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.",
|
||||
|
|
@ -1904,6 +1908,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",
|
||||
|
|
@ -2220,6 +2225,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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue