Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 39 additions & 9 deletions pkg/giturl/giturl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand All @@ -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")
Expand Down Expand Up @@ -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<commit>&line=N&lineEnd=N+1&lineStartColumn=1
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
57 changes: 54 additions & 3 deletions pkg/giturl/giturl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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",
Expand Down
Loading