Skip to content
Merged
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
37 changes: 37 additions & 0 deletions bitbucket/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package bitbucket

import (
"fmt"
"strconv"

"github.com/git-pkgs/forge"
)

// ParsePath implements Forge.ParsePath for Bitbucket URLs.
func (f *bitbucketForge) ParsePath(parts []string) (*forges.ResourceRef, error) {
if len(parts) < 2 {
return nil, fmt.Errorf("URL path must contain owner/repo")
}

ref := &forges.ResourceRef{
Owner: parts[0],
Repo: parts[1],
}

if len(parts) >= 4 {
num, err := strconv.Atoi(parts[3])
if err != nil {
return nil, fmt.Errorf("invalid number %q", parts[3])
}
ref.Number = num

switch parts[2] {
case "pull-requests":
ref.Type = forges.ResourceTypePR
case "issues":
ref.Type = forges.ResourceTypeIssue
}
}

return ref, nil
}
75 changes: 75 additions & 0 deletions bitbucket/url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package bitbucket

import (
"testing"

forges "github.com/git-pkgs/forge"
)

func TestParsePath(t *testing.T) {
tests := []struct {
name string
parts []string
wantOwner string
wantRepo string
wantResource forges.ResourceType
wantNumber int
wantErr bool
}{
{
name: "repo only",
parts: []string{"owner", "repo"},
wantOwner: "owner", wantRepo: "repo",
},
{
name: "pull request",
parts: []string{"owner", "repo", "pull-requests", "123"},
wantOwner: "owner", wantRepo: "repo",
wantResource: forges.ResourceTypePR, wantNumber: 123,
},
{
name: "issue",
parts: []string{"owner", "repo", "issues", "456"},
wantOwner: "owner", wantRepo: "repo",
wantResource: forges.ResourceTypeIssue, wantNumber: 456,
},
{
name: "missing repo",
parts: []string{"owner"},
wantErr: true,
},
{
name: "invalid PR number",
parts: []string{"owner", "repo", "pull-requests", "abc"},
wantErr: true,
},
}

f := &bitbucketForge{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref, err := f.ParsePath(tt.parts)
if tt.wantErr {
if err == nil {
t.Fatal("expected error")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ref.Owner != tt.wantOwner {
t.Errorf("owner: got %q, want %q", ref.Owner, tt.wantOwner)
}
if ref.Repo != tt.wantRepo {
t.Errorf("repo: got %q, want %q", ref.Repo, tt.wantRepo)
}
if ref.Type != tt.wantResource {
t.Errorf("resource: got %q, want %q", ref.Type, tt.wantResource)
}
if ref.Number != tt.wantNumber {
t.Errorf("number: got %d, want %d", ref.Number, tt.wantNumber)
}
})
}
}
18 changes: 18 additions & 0 deletions forge.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ var ErrNotSupported = errors.New("not supported by this forge")
// ErrLabelExists is returned when creating a label that already exists.
var ErrLabelExists = errors.New("label already exists")

// ResourceType identifies the kind of resource a URL points to.
type ResourceType string

const (
ResourceTypePR ResourceType = "pr"
ResourceTypeIssue ResourceType = "issue"
)

// ResourceRef identifies a resource (PR, issue, etc.) within a repository.
type ResourceRef struct {
Owner string
Repo string
Type ResourceType
Number int
}

// HTTPError represents a non-OK HTTP response from a forge API.
type HTTPError struct {
StatusCode int
Expand Down Expand Up @@ -52,6 +68,8 @@ type Forge interface {
Collaborators() CollaboratorService
CommitStatuses() CommitStatusService
GetRateLimit(ctx context.Context) (*RateLimit, error)
// ParsePath parses URL path segments into a resource reference.
ParsePath(pathParts []string) (*ResourceRef, error)
}

// Client routes requests to the appropriate Forge based on the URL domain.
Expand Down
4 changes: 4 additions & 0 deletions forges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ func (m *mockForge) GetRateLimit(_ context.Context) (*RateLimit, error) {
return nil, ErrNotSupported
}

func (m *mockForge) ParsePath(_ []string) (*ResourceRef, error) {
return &ResourceRef{}, nil
}

type mockFileService struct{}

func (m *mockFileService) Get(_ context.Context, _, _, _, _ string) (*FileContent, error) {
Expand Down
37 changes: 37 additions & 0 deletions gitea/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gitea

import (
"fmt"
"strconv"

"github.com/git-pkgs/forge"
)

// ParsePath implements Forge.ParsePath for Gitea/Forgejo URLs.
func (f *giteaForge) ParsePath(parts []string) (*forges.ResourceRef, error) {
if len(parts) < 2 {
return nil, fmt.Errorf("URL path must contain owner/repo")
}

ref := &forges.ResourceRef{
Owner: parts[0],
Repo: parts[1],
}

if len(parts) >= 4 {
num, err := strconv.Atoi(parts[3])
if err != nil {
return nil, fmt.Errorf("invalid number %q", parts[3])
}
ref.Number = num

switch parts[2] {
case "pulls":
ref.Type = forges.ResourceTypePR
case "issues":
ref.Type = forges.ResourceTypeIssue
}
}

return ref, nil
}
75 changes: 75 additions & 0 deletions gitea/url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package gitea

import (
"testing"

forges "github.com/git-pkgs/forge"
)

func TestParsePath(t *testing.T) {
tests := []struct {
name string
parts []string
wantOwner string
wantRepo string
wantResource forges.ResourceType
wantNumber int
wantErr bool
}{
{
name: "repo only",
parts: []string{"owner", "repo"},
wantOwner: "owner", wantRepo: "repo",
},
{
name: "pull request",
parts: []string{"owner", "repo", "pulls", "123"},
wantOwner: "owner", wantRepo: "repo",
wantResource: forges.ResourceTypePR, wantNumber: 123,
},
{
name: "issue",
parts: []string{"owner", "repo", "issues", "456"},
wantOwner: "owner", wantRepo: "repo",
wantResource: forges.ResourceTypeIssue, wantNumber: 456,
},
{
name: "missing repo",
parts: []string{"owner"},
wantErr: true,
},
{
name: "invalid PR number",
parts: []string{"owner", "repo", "pulls", "abc"},
wantErr: true,
},
}

f := &giteaForge{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref, err := f.ParsePath(tt.parts)
if tt.wantErr {
if err == nil {
t.Fatal("expected error")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ref.Owner != tt.wantOwner {
t.Errorf("owner: got %q, want %q", ref.Owner, tt.wantOwner)
}
if ref.Repo != tt.wantRepo {
t.Errorf("repo: got %q, want %q", ref.Repo, tt.wantRepo)
}
if ref.Type != tt.wantResource {
t.Errorf("resource: got %q, want %q", ref.Type, tt.wantResource)
}
if ref.Number != tt.wantNumber {
t.Errorf("number: got %d, want %d", ref.Number, tt.wantNumber)
}
})
}
}
37 changes: 37 additions & 0 deletions github/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package github

import (
"fmt"
"strconv"

forges "github.com/git-pkgs/forge"
)

// ParsePath implements Forge.ParsePath for GitHub URLs.
func (f *gitHubForge) ParsePath(parts []string) (*forges.ResourceRef, error) {
if len(parts) < 2 {
return nil, fmt.Errorf("URL path must contain owner/repo")
}

ref := &forges.ResourceRef{
Owner: parts[0],
Repo: parts[1],
}

if len(parts) >= 4 {
num, err := strconv.Atoi(parts[3])
if err != nil {
return nil, fmt.Errorf("invalid number %q", parts[3])
}
ref.Number = num

switch parts[2] {
case "pull":
ref.Type = forges.ResourceTypePR
case "issues":
ref.Type = forges.ResourceTypeIssue
}
}

return ref, nil
}
81 changes: 81 additions & 0 deletions github/url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package github

import (
"testing"

forges "github.com/git-pkgs/forge"
)

func TestParsePath(t *testing.T) {
tests := []struct {
name string
parts []string
wantOwner string
wantRepo string
wantResource forges.ResourceType
wantNumber int
wantErr bool
}{
{
name: "repo only",
parts: []string{"owner", "repo"},
wantOwner: "owner", wantRepo: "repo",
},
{
name: "pull request",
parts: []string{"owner", "repo", "pull", "123"},
wantOwner: "owner", wantRepo: "repo",
wantResource: forges.ResourceTypePR, wantNumber: 123,
},
{
name: "pull request with extra path",
parts: []string{"owner", "repo", "pull", "123", "files"},
wantOwner: "owner", wantRepo: "repo",
wantResource: forges.ResourceTypePR, wantNumber: 123,
},
{
name: "issue",
parts: []string{"owner", "repo", "issues", "456"},
wantOwner: "owner", wantRepo: "repo",
wantResource: forges.ResourceTypeIssue, wantNumber: 456,
},
{
name: "missing repo",
parts: []string{"owner"},
wantErr: true,
},
{
name: "invalid PR number",
parts: []string{"owner", "repo", "pull", "abc"},
wantErr: true,
},
}

f := &gitHubForge{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref, err := f.ParsePath(tt.parts)
if tt.wantErr {
if err == nil {
t.Fatal("expected error")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ref.Owner != tt.wantOwner {
t.Errorf("owner: got %q, want %q", ref.Owner, tt.wantOwner)
}
if ref.Repo != tt.wantRepo {
t.Errorf("repo: got %q, want %q", ref.Repo, tt.wantRepo)
}
if ref.Type != tt.wantResource {
t.Errorf("resource: got %q, want %q", ref.Type, tt.wantResource)
}
if ref.Number != tt.wantNumber {
t.Errorf("number: got %d, want %d", ref.Number, tt.wantNumber)
}
})
}
}
Loading
Loading