diff --git a/pkg/giturl/giturl.go b/pkg/giturl/giturl.go index 839f3f1a661f..ad8574bb0f7b 100644 --- a/pkg/giturl/giturl.go +++ b/pkg/giturl/giturl.go @@ -15,10 +15,11 @@ import ( type provider string const ( - providerGithub provider = "Github" - providerGitlab provider = "Gitlab" - providerBitbucket provider = "Bitbucket" - providerAzure provider = "Azure" + providerGithub provider = "Github" + providerGitlab provider = "Gitlab" + providerBitbucket provider = "Bitbucket" + providerBitbucketServer provider = "BitbucketServer" + providerAzure provider = "Azure" urlGithub = "github.com/" urlGitlab = "gitlab.com/" @@ -36,11 +37,25 @@ func determineProvider(repo string) provider { return providerBitbucket case strings.Contains(repo, urlAzure): return providerAzure + case isBitbucketServerURL(repo): + return providerBitbucketServer default: return "" } } +// isBitbucketServerURL detects Bitbucket Server/Data Center URLs by their +// distinctive /projects/{KEY}/repos/{SLUG}/... path structure. +// See: https://developer.atlassian.com/server/bitbucket/rest/v1002/intro/#about +// See: https://community.atlassian.com/forums/Bitbucket-questions/Bitbucket-Hyperlinking-to-source-code-in-Bitbucket/qaq-p/618967 +func isBitbucketServerURL(rawURL string) bool { + u, err := url.Parse(rawURL) + if err != nil { + return false + } + return strings.Contains(u.Path, "/projects/") && strings.Contains(u.Path, "/repos/") +} + func NormalizeBitbucketRepo(repoURL string) (string, error) { if !strings.HasPrefix(repoURL, "https") { return "", errors.New("Bitbucket requires https repo urls: e.g. https://bitbucket.org/org/repo.git") @@ -127,7 +142,11 @@ func GenerateLink(repo, commit, file string, line int64) string { switch determineProvider(repo) { case providerBitbucket: - return repo[:len(repo)-4] + "/commits/" + commit + baseLink := repo[:len(repo)-4] + "/src/" + commit + "/" + file + if line > 0 { + baseLink += "#lines-" + strconv.FormatInt(line, 10) + } + return baseLink case providerAzure: // Azure Repos format: ?path=/file&version=GC&line=N&lineEnd=N+1&lineStartColumn=1 @@ -192,7 +211,10 @@ func GenerateLink(repo, commit, file string, line int64) string { } } -var linePattern = regexp.MustCompile(`L\d+`) +var ( + linePattern = regexp.MustCompile(`L\d+`) + bbCloudLinePattern = regexp.MustCompile(`lines-\d+(:\d+)?`) +) // UpdateLinkLineNumber updates the line number in a repository link. // Used post-link generation to refine reported issue locations within large scanned blocks. @@ -213,9 +235,17 @@ func UpdateLinkLineNumber(ctx context.Context, link string, newLine int64) strin switch determineProvider(link) { case providerBitbucket: - // For Bitbucket, it doesn't support line links (based on the GenerateLink function). - // So we don't need to change anything. - return link + // Bitbucket Cloud uses #lines-N format for source file views. + fragment := "lines-" + strconv.FormatInt(newLine, 10) + if bbCloudLinePattern.MatchString(parsedURL.Fragment) { + parsedURL.Fragment = bbCloudLinePattern.ReplaceAllString(parsedURL.Fragment, fragment) + } else { + parsedURL.Fragment = fragment + } + + case providerBitbucketServer: + // Bitbucket Server/Data Center uses a bare line number as fragment: #N + parsedURL.Fragment = strconv.FormatInt(newLine, 10) case providerAzure: // For Azure, line numbers use query parameters: ?line=N&lineEnd=N+1&lineStartColumn=1 diff --git a/pkg/giturl/giturl_test.go b/pkg/giturl/giturl_test.go index 1839dc020e98..bdf382850be1 100644 --- a/pkg/giturl/giturl_test.go +++ b/pkg/giturl/giturl_test.go @@ -171,6 +171,33 @@ func TestGenerateLink(t *testing.T) { }, want: "https://dev.azure.com/org/project/_git/repo?version=GCabcdef", }, + { + name: "bitbucket cloud link gen", + args: args{ + repo: "https://bitbucket.org/org/repo.git", + commit: "abc123", + file: "main.go", + }, + want: "https://bitbucket.org/org/repo/src/abc123/main.go", + }, + { + name: "bitbucket cloud link gen with line", + args: args{ + repo: "https://bitbucket.org/org/repo.git", + commit: "abc123", + file: "main.go", + line: int64(19), + }, + want: "https://bitbucket.org/org/repo/src/abc123/main.go#lines-19", + }, + { + name: "bitbucket cloud link gen - no file", + args: args{ + repo: "https://bitbucket.org/org/repo.git", + commit: "abc123", + }, + want: "https://bitbucket.org/org/repo/src/abc123/", + }, { name: "Unknown provider on-prem instance", args: args{ @@ -270,12 +297,36 @@ func TestUpdateLinkLineNumber(t *testing.T) { wantErr bool }{ { - name: "Update bitbucket, no line number supported", + name: "Update Bitbucket Cloud link with line", args: args{ - link: "https://bitbucket.org/org/repo/blob/xyz123/main.go", + link: "https://bitbucket.org/org/repo/src/xyz123/main.go", newLine: int64(10), }, - want: "https://bitbucket.org/org/repo/blob/xyz123/main.go", + want: "https://bitbucket.org/org/repo/src/xyz123/main.go#lines-10", + }, + { + name: "Update Bitbucket Cloud link - replace existing line", + args: args{ + link: "https://bitbucket.org/org/repo/src/xyz123/main.go#lines-5", + newLine: int64(10), + }, + want: "https://bitbucket.org/org/repo/src/xyz123/main.go#lines-10", + }, + { + name: "Update Bitbucket Server link with line", + args: args{ + link: "https://enterprise.example.com/projects/PROJ/repos/repo/browse/main.go?at=xyz123", + newLine: int64(10), + }, + want: "https://enterprise.example.com/projects/PROJ/repos/repo/browse/main.go?at=xyz123#10", + }, + { + name: "Update Bitbucket Server link - replace existing line", + args: args{ + link: "https://enterprise.example.com/projects/PROJ/repos/repo/browse/main.go?at=xyz123#5", + newLine: int64(20), + }, + want: "https://enterprise.example.com/projects/PROJ/repos/repo/browse/main.go?at=xyz123#20", }, { name: "Update github link with line",