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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version: '1.25'
cache: true

- name: Download dependencies
Expand Down
153 changes: 77 additions & 76 deletions cmd/prx/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Package main provides the prx command-line tool for analyzing GitHub pull requests.
// Package main provides the prx command-line tool for analyzing pull requests
// from GitHub, GitLab, and Codeberg/Gitea.
package main

import (
Expand All @@ -7,41 +8,43 @@ import (
"errors"
"flag"
"fmt"
"log"
"log/slog"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"time"

"github.com/codeGROOVE-dev/fido/pkg/store/null"
"github.com/codeGROOVE-dev/prx/pkg/prx"
"github.com/codeGROOVE-dev/prx/pkg/prx/auth"
"github.com/codeGROOVE-dev/prx/pkg/prx/gitea"
"github.com/codeGROOVE-dev/prx/pkg/prx/github"
"github.com/codeGROOVE-dev/prx/pkg/prx/gitlab"
)

const (
expectedURLParts = 4
pullPathIndex = 2
pullPathValue = "pull"
var (
debug = flag.Bool("debug", false, "Enable debug logging")
noCache = flag.Bool("no-cache", false, "Disable caching")
referenceTimeStr = flag.String("reference-time", "", "Reference time for cache validation (RFC3339 format)")
)

func main() {
debug := flag.Bool("debug", false, "Enable debug logging")
noCache := flag.Bool("no-cache", false, "Disable caching")
referenceTimeStr := flag.String("reference-time", "", "Reference time for cache validation (RFC3339 format, e.g., 2025-03-16T06:18:08Z)")
flag.Parse()

if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "prx: %v\n", err)
os.Exit(1)
}
}

func run() error {
if *debug {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
}

if flag.NArg() != 1 {
fmt.Fprintf(os.Stderr, "Usage: %s [--debug] [--no-cache] [--reference-time=TIME] <pull-request-url>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Example: %s https://github.com/golang/go/pull/12345\n", os.Args[0])
os.Exit(1)
printUsage()
return errors.New("expected exactly one argument (pull request URL)")
}

// Parse reference time if provided
Expand All @@ -50,91 +53,89 @@ func main() {
var err error
referenceTime, err = time.Parse(time.RFC3339, *referenceTimeStr)
if err != nil {
log.Printf("Invalid reference time format (use RFC3339, e.g., 2025-03-16T06:18:08Z): %v", err)
os.Exit(1)
return fmt.Errorf("invalid reference time format (use RFC3339, e.g., 2025-03-16T06:18:08Z): %w", err)
}
}

prURL := flag.Arg(0)

owner, repo, prNumber, err := parsePRURL(prURL)
parsed, err := prx.ParseURL(prURL)
if err != nil {
log.Printf("Invalid PR URL: %v", err)
os.Exit(1)
return fmt.Errorf("invalid PR URL: %w", err)
}

token, err := githubToken()
if err != nil {
log.Printf("Failed to get GitHub token: %v", err)
os.Exit(1)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

// Resolve token based on platform
resolver := auth.NewResolver()
platform := auth.DetectPlatform(parsed.Platform)

token, err := resolver.Resolve(ctx, platform, parsed.Host)
// Authentication is optional for public repos on GitLab/Gitea/Codeberg
// Only GitHub strictly requires authentication for most API calls
tokenOptional := parsed.Platform != prx.PlatformGitHub

if err != nil && !tokenOptional {
return fmt.Errorf("authentication failed: %w", err)
}

var opts []prx.Option
if *debug {
opts = append(opts, prx.WithLogger(slog.Default()))
var tokenValue string
if token != nil {
tokenValue = token.Value
slog.Debug("Using token", "source", token.Source, "host", token.Host)
} else {
slog.Debug("No authentication token found, proceeding without authentication")
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
// Create platform-specific client
var prxPlatform prx.Platform
switch parsed.Platform {
case prx.PlatformGitHub:
prxPlatform = github.NewPlatform(tokenValue)
case prx.PlatformGitLab:
prxPlatform = gitlab.NewPlatform(tokenValue, gitlab.WithBaseURL("https://"+parsed.Host))
case prx.PlatformCodeberg:
prxPlatform = gitea.NewCodebergPlatform(tokenValue)
default:
// Self-hosted Gitea
prxPlatform = gitea.NewPlatform(tokenValue, gitea.WithBaseURL("https://"+parsed.Host))
}

// Configure client options
var opts []prx.Option
if *debug {
opts = append(opts, prx.WithLogger(slog.Default()))
}
if *noCache {
opts = append(opts, prx.WithCacheStore(null.New[string, prx.PullRequestData]()))
}

client := prx.NewClient(token, opts...)
data, err := client.PullRequestWithReferenceTime(ctx, owner, repo, prNumber, referenceTime)
client := prx.NewClientWithPlatform(prxPlatform, opts...)
data, err := client.PullRequestWithReferenceTime(ctx, parsed.Owner, parsed.Repo, parsed.Number, referenceTime)
if err != nil {
log.Printf("Failed to fetch PR data: %v", err)
cancel()
os.Exit(1) //nolint:gocritic // False positive: cancel() is called immediately before os.Exit()
return fmt.Errorf("failed to fetch PR data: %w", err)
}

encoder := json.NewEncoder(os.Stdout)
if err := encoder.Encode(data); err != nil {
log.Printf("Failed to encode pull request: %v", err)
cancel()
os.Exit(1)
}

cancel() // Ensure context is cancelled before exit
}

func githubToken() (string, error) {
cmd := exec.CommandContext(context.Background(), "gh", "auth", "token")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to run 'gh auth token': %w", err)
}

token := strings.TrimSpace(string(output))
if token == "" {
return "", errors.New("no token returned by 'gh auth token'")
return fmt.Errorf("failed to encode pull request: %w", err)
}

return token, nil
return nil
}

//nolint:revive // function-result-limit: function needs all 4 return values
func parsePRURL(prURL string) (owner, repo string, prNumber int, err error) {
u, err := url.Parse(prURL)
if err != nil {
return "", "", 0, err
}

if u.Host != "github.com" {
return "", "", 0, errors.New("not a GitHub URL")
}

parts := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(parts) != expectedURLParts || parts[pullPathIndex] != pullPathValue {
return "", "", 0, errors.New("invalid PR URL format")
}

prNumber, err = strconv.Atoi(parts[3])
if err != nil {
return "", "", 0, fmt.Errorf("invalid PR number: %w", err)
}

return parts[0], parts[1], prNumber, nil
func printUsage() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pull-request-url>\n\n", os.Args[0])
fmt.Fprint(os.Stderr, "Supported platforms:\n")
fmt.Fprint(os.Stderr, " GitHub: https://github.com/owner/repo/pull/123\n")
fmt.Fprint(os.Stderr, " GitLab: https://gitlab.com/owner/repo/-/merge_requests/123\n")
fmt.Fprint(os.Stderr, " Codeberg: https://codeberg.org/owner/repo/pulls/123\n\n")
fmt.Fprint(os.Stderr, "Authentication:\n")
fmt.Fprint(os.Stderr, " GitHub: GITHUB_TOKEN env or 'gh auth login'\n")
fmt.Fprint(os.Stderr, " GitLab: GITLAB_TOKEN env or 'glab auth login'\n")
fmt.Fprint(os.Stderr, " Codeberg: CODEBERG_TOKEN env\n")
fmt.Fprint(os.Stderr, " Gitea: GITEA_TOKEN env\n\n")
fmt.Fprint(os.Stderr, "Options:\n")
flag.PrintDefaults()
}
5 changes: 3 additions & 2 deletions cmd/prx_compare/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"

"github.com/codeGROOVE-dev/prx/pkg/prx"
"github.com/codeGROOVE-dev/prx/pkg/prx/github"
)

const (
Expand All @@ -38,15 +39,15 @@ func main() {

// Both now use GraphQL, but we'll compare two fetches to ensure consistency
fmt.Println("Fetching first time...")
restClient := prx.NewClient(token)
restClient := prx.NewClientWithPlatform(github.NewPlatform(token))
restData, err := restClient.PullRequest(context.TODO(), owner, repo, prNumber)
if err != nil {
log.Fatalf("First fetch failed: %v", err)
}

// Fetch again to compare consistency
fmt.Println("Fetching second time...")
graphqlClient := prx.NewClient(token)
graphqlClient := prx.NewClientWithPlatform(github.NewPlatform(token))
graphqlData, err := graphqlClient.PullRequest(context.TODO(), owner, repo, prNumber)
if err != nil {
log.Fatalf("Second fetch failed: %v", err)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ require (
github.com/codeGROOVE-dev/fido/pkg/store/compress v1.10.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading