Skip to content

Commit 5fb3cb8

Browse files
committed
Add new Fetch API
1 parent b9dfd36 commit 5fb3cb8

10 files changed

Lines changed: 385 additions & 66 deletions

File tree

pkg/prx/example_test.go

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,15 @@ import (
44
"context"
55
"fmt"
66
"log"
7-
"os"
87

9-
"github.com/codeGROOVE-dev/prx/pkg/prx"
10-
"github.com/codeGROOVE-dev/prx/pkg/prx/github"
8+
"github.com/codeGROOVE-dev/prx/pkg/prx/fetch"
119
)
1210

1311
func Example() {
14-
// Create a client with your GitHub token
15-
token := os.Getenv("GITHUB_TOKEN")
16-
if token == "" {
17-
log.Fatal("GITHUB_TOKEN environment variable not set")
18-
}
19-
20-
client := prx.NewClientWithPlatform(github.NewPlatform(token))
21-
22-
// Fetch events for a pull request
12+
// The simplest way: just pass a URL
13+
// Authentication is automatically resolved from environment or CLI tools
2314
ctx := context.Background()
24-
data, err := client.PullRequest(ctx, "owner", "repo", 123)
15+
data, err := fetch.Fetch(ctx, "https://github.com/owner/repo/pull/123")
2516
if err != nil {
2617
log.Fatal(err)
2718
}
@@ -42,14 +33,52 @@ func Example() {
4233
}
4334
}
4435

45-
func ExampleClient_PullRequest() {
46-
// Create a client with custom logger
47-
token := os.Getenv("GITHUB_TOKEN")
48-
client := prx.NewClientWithPlatform(github.NewPlatform(token))
36+
func ExampleFetch() {
37+
ctx := context.Background()
38+
39+
// Works with GitHub
40+
data, err := fetch.Fetch(ctx, "https://github.com/owner/repo/pull/123")
41+
if err != nil {
42+
log.Fatal(err)
43+
}
44+
fmt.Printf("GitHub PR #%d\n", data.PullRequest.Number)
45+
46+
// Works with GitLab
47+
data, err = fetch.Fetch(ctx, "https://gitlab.com/owner/repo/-/merge_requests/456")
48+
if err != nil {
49+
log.Fatal(err)
50+
}
51+
fmt.Printf("GitLab MR #%d\n", data.PullRequest.Number)
52+
53+
// Works with Codeberg
54+
data, err = fetch.Fetch(ctx, "https://codeberg.org/owner/repo/pulls/789")
55+
if err != nil {
56+
log.Fatal(err)
57+
}
58+
fmt.Printf("Codeberg PR #%d\n", data.PullRequest.Number)
59+
60+
// Works with any Gitea instance
61+
data, err = fetch.Fetch(ctx, "https://gitea.example.com/owner/repo/pulls/100")
62+
if err != nil {
63+
log.Fatal(err)
64+
}
65+
fmt.Printf("Gitea PR #%d\n", data.PullRequest.Number)
4966

50-
// Fetch all events for PR #123
67+
// URL fragments and query parameters are automatically stripped
68+
data, err = fetch.Fetch(ctx, "https://github.com/owner/repo/pull/123?tab=checks#issuecomment-456")
69+
if err != nil {
70+
log.Fatal(err)
71+
}
72+
fmt.Printf("PR #%d (cleaned URL)\n", data.PullRequest.Number)
73+
}
74+
75+
func ExampleClient_PullRequest() {
76+
// For advanced use cases where you need to fetch multiple PRs from the same platform,
77+
// you can still use the client-based API
5178
ctx := context.Background()
52-
data, err := client.PullRequest(ctx, "golang", "go", 123)
79+
80+
// Fetch using the simple API
81+
data, err := fetch.Fetch(ctx, "https://github.com/golang/go/pull/123")
5382
if err != nil {
5483
log.Fatal(err)
5584
}

pkg/prx/fetch/fetch.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Package fetch provides convenience functions for fetching pull requests
2+
// with automatic platform detection and authentication resolution.
3+
package fetch
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"log/slog"
9+
10+
"github.com/codeGROOVE-dev/prx/pkg/prx"
11+
"github.com/codeGROOVE-dev/prx/pkg/prx/auth"
12+
"github.com/codeGROOVE-dev/prx/pkg/prx/gitea"
13+
"github.com/codeGROOVE-dev/prx/pkg/prx/github"
14+
"github.com/codeGROOVE-dev/prx/pkg/prx/gitlab"
15+
)
16+
17+
// Fetch automatically detects the platform from a PR/MR URL, resolves authentication,
18+
// and fetches the pull request data. This is the simplest way to use prx.
19+
//
20+
// Example:
21+
//
22+
// data, err := fetch.Fetch(ctx, "https://github.com/owner/repo/pull/123")
23+
// data, err := fetch.Fetch(ctx, "https://gitlab.com/owner/repo/-/merge_requests/456")
24+
// data, err := fetch.Fetch(ctx, "https://codeberg.org/owner/repo/pulls/789")
25+
// data, err := fetch.Fetch(ctx, "https://gitea.example.com/owner/repo/pulls/100")
26+
//
27+
// The function will:
28+
// - Parse the URL to detect platform, owner, repo, and PR number
29+
// - Automatically resolve authentication tokens from environment variables or CLI tools
30+
// - Create the appropriate platform client
31+
// - Fetch and return the pull request data
32+
//
33+
// Authentication is resolved in this order:
34+
// - GitHub: GITHUB_TOKEN, GH_TOKEN env vars, or 'gh auth token'
35+
// - GitLab: GITLAB_TOKEN, GL_TOKEN env vars, or 'glab config get token'
36+
// - Gitea/Codeberg: CODEBERG_TOKEN, GITEA_TOKEN env vars, tea config, or 'berg auth token'
37+
//
38+
// URL fragments (#...) and query parameters (?...) are automatically stripped.
39+
// Unknown hosts default to Gitea unless the URL indicates GitHub or GitLab.
40+
//
41+
// Use WithOptions to pass additional client configuration options.
42+
func Fetch(ctx context.Context, url string, opts ...prx.Option) (*prx.PullRequestData, error) {
43+
// Parse URL to get platform, owner, repo, PR number
44+
parsed, err := prx.ParseURL(url)
45+
if err != nil {
46+
return nil, fmt.Errorf("parsing URL: %w", err)
47+
}
48+
49+
// Resolve authentication token for the platform
50+
resolver := auth.NewResolver()
51+
authPlatform := auth.DetectPlatform(parsed.Platform)
52+
token, err := resolver.Resolve(ctx, authPlatform, parsed.Host)
53+
if err != nil {
54+
return nil, fmt.Errorf("resolving authentication for %s: %w", parsed.Platform, err)
55+
}
56+
57+
// Create platform-specific client
58+
var platform prx.Platform
59+
switch parsed.Platform {
60+
case prx.PlatformGitHub:
61+
platform = github.NewPlatform(token.Value)
62+
case prx.PlatformGitLab:
63+
platform = gitlab.NewPlatform(token.Value, gitlab.WithBaseURL("https://"+parsed.Host))
64+
case prx.PlatformCodeberg:
65+
platform = gitea.NewCodebergPlatform(token.Value)
66+
default:
67+
// Default to Gitea for unknown platforms
68+
platform = gitea.NewPlatform(token.Value, gitea.WithBaseURL("https://"+parsed.Host))
69+
}
70+
71+
// Create client and fetch PR
72+
client := prx.NewClientWithPlatform(platform, opts...)
73+
defer func() {
74+
if closeErr := client.Close(); closeErr != nil {
75+
slog.WarnContext(ctx, "failed to close client", "error", closeErr)
76+
}
77+
}()
78+
79+
return client.PullRequest(ctx, parsed.Owner, parsed.Repo, parsed.Number)
80+
}
81+
82+
// WithOptions is an alias for Fetch for backwards compatibility.
83+
//
84+
// Deprecated: Use Fetch directly, which now accepts options.
85+
func WithOptions(ctx context.Context, url string, opts ...prx.Option) (*prx.PullRequestData, error) {
86+
return Fetch(ctx, url, opts...)
87+
}

pkg/prx/fetch/fetch_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package fetch_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/codeGROOVE-dev/prx/pkg/prx/fetch"
8+
)
9+
10+
func TestFetch_InvalidURL(t *testing.T) {
11+
ctx := context.Background()
12+
13+
_, err := fetch.Fetch(ctx, "not-a-valid-url")
14+
if err == nil {
15+
t.Error("expected error for invalid URL, got nil")
16+
}
17+
}
18+
19+
func TestFetch_EmptyURL(t *testing.T) {
20+
ctx := context.Background()
21+
22+
_, err := fetch.Fetch(ctx, "")
23+
if err == nil {
24+
t.Error("expected error for empty URL, got nil")
25+
}
26+
}
27+
28+
func TestWithOptions_Compatibility(t *testing.T) {
29+
ctx := context.Background()
30+
31+
// Test the deprecated WithOptions function still works
32+
_, err := fetch.WithOptions(ctx, "invalid-url")
33+
if err == nil {
34+
t.Error("expected error for invalid URL, got nil")
35+
}
36+
}
37+
38+
// Note: This package is a thin convenience wrapper that combines:
39+
// - URL parsing (tested in pkg/prx/url_test.go)
40+
// - Authentication resolution (tested in pkg/prx/auth/auth_test.go)
41+
// - Platform client creation (tested in each platform package)
42+
// - Pull request fetching (tested in each platform package)
43+
//
44+
// Full integration tests would require real API tokens and network access.
45+
// The coverage here reflects that this is glue code - the real logic
46+
// is thoroughly tested in the underlying packages.

pkg/prx/gitea/platform.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -538,12 +538,16 @@ func convertToEvents(
538538
if commits[i].Author != nil {
539539
actor = commits[i].Author.Login
540540
}
541+
msg := commits[i].Commit.Message
542+
if idx := strings.IndexByte(msg, '\n'); idx >= 0 {
543+
msg = msg[:idx]
544+
}
541545
events = append(events, prx.Event{
542546
Timestamp: commits[i].Commit.Author.Date,
543547
Kind: prx.EventKindCommit,
544548
Actor: actor,
545549
Body: commits[i].SHA[:7],
546-
Description: firstLine(commits[i].Commit.Message),
550+
Description: msg,
547551
})
548552
}
549553

@@ -728,10 +732,3 @@ func convertTimelineEvent(event *timelineEvent) *prx.Event {
728732
}
729733

730734
// Helper functions.
731-
732-
func firstLine(s string) string {
733-
if idx := strings.IndexByte(s, '\n'); idx >= 0 {
734-
return s[:idx]
735-
}
736-
return s
737-
}

pkg/prx/gitea/platform_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import (
1212
"github.com/codeGROOVE-dev/prx/pkg/prx"
1313
)
1414

15+
// Test helper function
16+
func firstLine(s string) string {
17+
if idx := strings.IndexByte(s, '\n'); idx >= 0 {
18+
return s[:idx]
19+
}
20+
return s
21+
}
22+
1523
func TestPlatform_Name(t *testing.T) {
1624
tests := []struct {
1725
name string

pkg/prx/github/collaborators_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package github
22

33
import (
44
"context"
5+
"fmt"
56
"log/slog"
67
"testing"
78

@@ -10,6 +11,11 @@ import (
1011
"github.com/codeGROOVE-dev/fido"
1112
)
1213

14+
// Test helper for cache keys
15+
func collaboratorsCacheKey(owner, repo string) string {
16+
return fmt.Sprintf("%s/%s", owner, repo)
17+
}
18+
1319
// TestPermissionToWriteAccess tests permission level mapping
1420
func TestPermissionToWriteAccess(t *testing.T) {
1521
tests := []struct {

pkg/prx/github/platform.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ func (p *Platform) writeAccessFromAssociation(ctx context.Context, owner, repo,
741741

742742
// checkCollaboratorPermission checks if a user has write access.
743743
func (p *Platform) checkCollaboratorPermission(ctx context.Context, owner, repo, user string) int {
744-
collabs, err := p.collaboratorsCache.Fetch(collaboratorsCacheKey(owner, repo), func() (map[string]string, error) {
744+
collabs, err := p.collaboratorsCache.Fetch(fmt.Sprintf("%s/%s", owner, repo), func() (map[string]string, error) {
745745
result, fetchErr := p.client.Collaborators(ctx, owner, repo)
746746
if fetchErr != nil {
747747
p.logger.WarnContext(ctx, "failed to fetch collaborators for write access check",
@@ -886,9 +886,7 @@ func buildReviewersMap(data *graphQLPullRequestComplete) map[string]prx.ReviewSt
886886

887887
// fetchRulesetsREST fetches repository rulesets via REST API.
888888
func (p *Platform) fetchRulesetsREST(ctx context.Context, owner, repo string) ([]string, error) {
889-
cacheKey := rulesetsCacheKey(owner, repo)
890-
891-
return p.rulesetsCache.Fetch(cacheKey, func() ([]string, error) {
889+
return p.rulesetsCache.Fetch(fmt.Sprintf("%s/%s", owner, repo), func() ([]string, error) {
892890
path := fmt.Sprintf("/repos/%s/%s/rulesets", owner, repo)
893891
var rulesets []Ruleset
894892

@@ -923,7 +921,7 @@ func (p *Platform) fetchCheckRunsREST(ctx context.Context, owner, repo, sha stri
923921
return nil, nil
924922
}
925923

926-
cacheKey := checkRunsCacheKey(owner, repo, sha)
924+
cacheKey := fmt.Sprintf("%s/%s/%s", owner, repo, sha)
927925

928926
if cached, ok := p.checkRunsCache.Get(cacheKey); ok {
929927
if !cached.CachedAt.Before(refTime) {
@@ -1090,18 +1088,3 @@ func (*Platform) calculateTestStateFromCheckSummary(summary *prx.CheckSummary) s
10901088

10911089
return prx.TestStateNone
10921090
}
1093-
1094-
// collaboratorsCacheKey generates a cache key for collaborators data.
1095-
func collaboratorsCacheKey(owner, repo string) string {
1096-
return fmt.Sprintf("%s/%s", owner, repo)
1097-
}
1098-
1099-
// rulesetsCacheKey generates a cache key for rulesets data.
1100-
func rulesetsCacheKey(owner, repo string) string {
1101-
return fmt.Sprintf("%s/%s", owner, repo)
1102-
}
1103-
1104-
// checkRunsCacheKey generates a cache key for check runs data.
1105-
func checkRunsCacheKey(owner, repo, sha string) string {
1106-
return fmt.Sprintf("%s/%s/%s", owner, repo, sha)
1107-
}

0 commit comments

Comments
 (0)