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
4 changes: 4 additions & 0 deletions bitbucket/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ func New(token string, hc *http.Client) forge.Forge {
return &bitbucketForge{token: token, httpClient: hc}
}

func (f *bitbucketForge) APIBaseURL() string {
return bitbucketAPI
}

type bitbucketRepoService struct {
token string
httpClient *http.Client
Expand Down
6 changes: 6 additions & 0 deletions forge.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ func (e *HTTPError) Error() string {
return fmt.Sprintf("forge: HTTP %d from %s", e.StatusCode, e.URL)
}

// APIBaseURLProvider is implemented by forge backends that can expose their
// raw API root URL for arbitrary endpoint requests.
type APIBaseURLProvider interface {
APIBaseURL() string
}

// Forge is the interface each forge backend implements.
type Forge interface {
Repos() RepoService
Expand Down
5 changes: 5 additions & 0 deletions gitea/gitea.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
forge "github.com/git-pkgs/forge"
"net/http"
"strings"

"code.gitea.io/sdk/gitea"
)
Expand Down Expand Up @@ -34,6 +35,10 @@ func New(baseURL, token string, hc *http.Client) forge.Forge {
return &giteaForge{client: c, baseURL: baseURL, token: token, httpClient: hc}
}

func (f *giteaForge) APIBaseURL() string {
return strings.TrimRight(f.baseURL, "/") + "/api/v1"
}

type giteaRepoService struct {
client *gitea.Client
}
Expand Down
12 changes: 9 additions & 3 deletions github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import (
"context"
forge "github.com/git-pkgs/forge"
"net/http"
"strings"

"github.com/google/go-github/v82/github"
)

const defaultPageSize = 100

type gitHubForge struct {
client *github.Client
client *github.Client
apiBaseURL string
}

// New creates a GitHub forge backend for github.com.
Expand All @@ -20,14 +22,18 @@ func New(token string, hc *http.Client) forge.Forge {
if token != "" {
c = c.WithAuthToken(token)
}
return &gitHubForge{client: c}
return &gitHubForge{client: c, apiBaseURL: "https://api.github.com"}
}

// NewWithBase creates a GitHub forge backend for a GitHub Enterprise instance.
func NewWithBase(baseURL, token string, hc *http.Client) forge.Forge {
c := github.NewClient(hc).WithAuthToken(token)
c, _ = c.WithEnterpriseURLs(baseURL, baseURL)
return &gitHubForge{client: c}
return &gitHubForge{client: c, apiBaseURL: strings.TrimRight(baseURL, "/") + "/api/v3"}
}

func (f *gitHubForge) APIBaseURL() string {
return f.apiBaseURL
}

type gitHubRepoService struct {
Expand Down
13 changes: 10 additions & 3 deletions gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,31 @@ import (
"context"
forge "github.com/git-pkgs/forge"
"net/http"
"strings"

gitlab "gitlab.com/gitlab-org/api/client-go"
)

type gitLabForge struct {
client *gitlab.Client
client *gitlab.Client
apiBaseURL string
}

// New creates a GitLab forge backend.
func New(baseURL, token string, hc *http.Client) forge.Forge {
apiBaseURL := strings.TrimRight(baseURL, "/") + "/api/v4"
opts := []gitlab.ClientOptionFunc{
gitlab.WithBaseURL(baseURL + "/api/v4"),
gitlab.WithBaseURL(apiBaseURL),
}
if hc != nil {
opts = append(opts, gitlab.WithHTTPClient(hc))
}
c, _ := gitlab.NewClient(token, opts...)
return &gitLabForge{client: c}
return &gitLabForge{client: c, apiBaseURL: apiBaseURL}
}

func (f *gitLabForge) APIBaseURL() string {
return f.apiBaseURL
}

type gitLabRepoService struct {
Expand Down
41 changes: 26 additions & 15 deletions internal/cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"strings"

forges "github.com/git-pkgs/forge"
"github.com/git-pkgs/forge/internal/resolve"
"github.com/spf13/cobra"
)
Expand All @@ -23,7 +24,7 @@ var apiCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
endpoint := args[0]

_, owner, repoName, domain, err := resolve.Repo(flagRepo, flagForgeType)
forge, owner, repoName, domain, err := resolve.Repo(flagRepo, flagForgeType)
if err != nil {
return err
}
Expand All @@ -32,20 +33,8 @@ var apiCmd = &cobra.Command{
endpoint = strings.ReplaceAll(endpoint, "{owner}", owner)
endpoint = strings.ReplaceAll(endpoint, "{repo}", repoName)

// Build full URL
var baseURL string
switch {
case strings.Contains(domain, "github"):
baseURL = "https://api." + domain
case strings.Contains(domain, "gitlab"):
baseURL = "https://" + domain + "/api/v4"
case strings.Contains(domain, "bitbucket"):
baseURL = "https://api.bitbucket.org/2.0"
default:
baseURL = "https://" + domain + "/api/v1"
}

url := baseURL + "/" + strings.TrimLeft(endpoint, "/")
baseURL := apiBaseURL(forge, domain)
url := strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(endpoint, "/")

var body io.Reader
if len(flagAPIFields) > 0 {
Expand Down Expand Up @@ -129,6 +118,28 @@ var apiCmd = &cobra.Command{
},
}

func apiBaseURL(forge forges.Forge, domain string) string {
provider, ok := forge.(forges.APIBaseURLProvider)
if ok {
return provider.APIBaseURL()
}

return legacyAPIBaseURL(domain)
}

func legacyAPIBaseURL(domain string) string {
switch {
case strings.Contains(domain, "github"):
return "https://api." + domain
case strings.Contains(domain, "gitlab"):
return "https://" + domain + "/api/v4"
case strings.Contains(domain, "bitbucket"):
return "https://api.bitbucket.org/2.0"
default:
return "https://" + domain + "/api/v1"
}
}

var (
flagAPIMethod string
flagAPIFields []string
Expand Down
139 changes: 139 additions & 0 deletions internal/cli/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package cli

import (
"os"
"path/filepath"
"testing"

forges "github.com/git-pkgs/forge"
"github.com/git-pkgs/forge/bitbucket"
"github.com/git-pkgs/forge/gitea"
ghforge "github.com/git-pkgs/forge/github"
glforge "github.com/git-pkgs/forge/gitlab"
"github.com/git-pkgs/forge/internal/config"
"github.com/git-pkgs/forge/internal/resolve"
)

func TestAPIBaseURLFromForge(t *testing.T) {
tests := []struct {
name string
f forges.Forge
want string
}{
{
name: "github",
f: ghforge.New("", nil),
want: "https://api.github.com",
},
{
name: "github enterprise",
f: ghforge.NewWithBase("https://github.enterprise.example.org", "", nil),
want: "https://github.enterprise.example.org/api/v3",
},
{
name: "gitlab",
f: glforge.New("https://mylab.example.org", "", nil),
want: "https://mylab.example.org/api/v4",
},
{
name: "gitea",
f: gitea.New("https://gitea.example.org", "", nil),
want: "https://gitea.example.org/api/v1",
},
{
name: "forgejo",
f: gitea.New("https://forgejo.example.org", "", nil),
want: "https://forgejo.example.org/api/v1",
},
{
name: "bitbucket",
f: bitbucket.New("", nil),
want: "https://api.bitbucket.org/2.0",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := apiBaseURL(tt.f, "fallback.example.org")
if got != tt.want {
t.Fatalf("APIBaseURL() = %q, want %q", got, tt.want)
}
})
}
}

func TestAPIBaseURLFallsBackToLegacyDomainHeuristics(t *testing.T) {
tests := []struct {
name string
domain string
want string
}{
{
name: "github",
domain: "github.example.org",
want: "https://api.github.example.org",
},
{
name: "gitlab",
domain: "gitlab.example.org",
want: "https://gitlab.example.org/api/v4",
},
{
name: "bitbucket",
domain: "bitbucket.example.org",
want: "https://api.bitbucket.org/2.0",
},
{
name: "default",
domain: "forge.example.org",
want: "https://forge.example.org/api/v1",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := apiBaseURL(&mockForge{}, tt.domain)
if got != tt.want {
t.Fatalf("apiBaseURL(..., %q) = %q, want %q", tt.domain, got, tt.want)
}
})
}
}

func TestAPIBaseURLUsesLegacyFallbackForUnknownDomainWithoutConfiguredForgeType(t *testing.T) {
got := apiBaseURL(&mockForge{}, "mylab.example.org")
want := "https://mylab.example.org/api/v1"
if got != want {
t.Fatalf("apiBaseURL(..., %q) = %q, want %q", "mylab.example.org", got, want)
}
}

func TestAPIBaseURLUsesConfiguredForgeTypeForUnknownDomain(t *testing.T) {
config.ResetCache()
defer config.ResetCache()

xdgConfigHome := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", xdgConfigHome)

configDir := filepath.Join(xdgConfigHome, "forge")
if err := os.MkdirAll(configDir, 0700); err != nil {
t.Fatal(err)
}
configPath := filepath.Join(configDir, "config")
if err := os.WriteFile(configPath, []byte(`[mylab.example.org]
type = gitlab
`), 0600); err != nil {
t.Fatal(err)
}

forge, _, _, domain, err := resolve.Repo("mylab.example.org/imt/deployments", "")
if err != nil {
t.Fatal(err)
}

got := apiBaseURL(forge, domain)
want := "https://mylab.example.org/api/v4"
if got != want {
t.Fatalf("apiBaseURL(..., %q) = %q, want %q", domain, got, want)
}
}