1- // Package main provides the prx command-line tool for analyzing GitHub pull requests.
1+ // Package main provides the prx command-line tool for analyzing pull requests
2+ // from GitHub, GitLab, and Codeberg/Gitea.
23package main
34
45import (
@@ -7,41 +8,43 @@ import (
78 "errors"
89 "flag"
910 "fmt"
10- "log"
1111 "log/slog"
12- "net/url"
1312 "os"
14- "os/exec"
15- "strconv"
16- "strings"
1713 "time"
1814
1915 "github.com/codeGROOVE-dev/fido/pkg/store/null"
2016 "github.com/codeGROOVE-dev/prx/pkg/prx"
17+ "github.com/codeGROOVE-dev/prx/pkg/prx/auth"
18+ "github.com/codeGROOVE-dev/prx/pkg/prx/gitea"
19+ "github.com/codeGROOVE-dev/prx/pkg/prx/github"
20+ "github.com/codeGROOVE-dev/prx/pkg/prx/gitlab"
2121)
2222
23- const (
24- expectedURLParts = 4
25- pullPathIndex = 2
26- pullPathValue = "pull"
23+ var (
24+ debug = flag . Bool ( "debug" , false , "Enable debug logging" )
25+ noCache = flag . Bool ( "no-cache" , false , "Disable caching" )
26+ referenceTimeStr = flag . String ( "reference-time" , "" , "Reference time for cache validation (RFC3339 format)" )
2727)
2828
2929func main () {
30- debug := flag .Bool ("debug" , false , "Enable debug logging" )
31- noCache := flag .Bool ("no-cache" , false , "Disable caching" )
32- referenceTimeStr := flag .String ("reference-time" , "" , "Reference time for cache validation (RFC3339 format, e.g., 2025-03-16T06:18:08Z)" )
3330 flag .Parse ()
3431
32+ if err := run (); err != nil {
33+ fmt .Fprintf (os .Stderr , "prx: %v\n " , err )
34+ os .Exit (1 )
35+ }
36+ }
37+
38+ func run () error {
3539 if * debug {
3640 slog .SetDefault (slog .New (slog .NewTextHandler (os .Stderr , & slog.HandlerOptions {
3741 Level : slog .LevelDebug ,
3842 })))
3943 }
4044
4145 if flag .NArg () != 1 {
42- fmt .Fprintf (os .Stderr , "Usage: %s [--debug] [--no-cache] [--reference-time=TIME] <pull-request-url>\n " , os .Args [0 ])
43- fmt .Fprintf (os .Stderr , "Example: %s https://github.com/golang/go/pull/12345\n " , os .Args [0 ])
44- os .Exit (1 )
46+ printUsage ()
47+ return errors .New ("expected exactly one argument (pull request URL)" )
4548 }
4649
4750 // Parse reference time if provided
@@ -50,91 +53,89 @@ func main() {
5053 var err error
5154 referenceTime , err = time .Parse (time .RFC3339 , * referenceTimeStr )
5255 if err != nil {
53- log .Printf ("Invalid reference time format (use RFC3339, e.g., 2025-03-16T06:18:08Z): %v" , err )
54- os .Exit (1 )
56+ return fmt .Errorf ("invalid reference time format (use RFC3339, e.g., 2025-03-16T06:18:08Z): %w" , err )
5557 }
5658 }
5759
5860 prURL := flag .Arg (0 )
5961
60- owner , repo , prNumber , err := parsePRURL (prURL )
62+ parsed , err := prx . ParseURL (prURL )
6163 if err != nil {
62- log .Printf ("Invalid PR URL: %v" , err )
63- os .Exit (1 )
64+ return fmt .Errorf ("invalid PR URL: %w" , err )
6465 }
6566
66- token , err := githubToken ()
67- if err != nil {
68- log .Printf ("Failed to get GitHub token: %v" , err )
69- os .Exit (1 )
67+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Minute )
68+ defer cancel ()
69+
70+ // Resolve token based on platform
71+ resolver := auth .NewResolver ()
72+ platform := auth .DetectPlatform (parsed .Platform )
73+
74+ token , err := resolver .Resolve (ctx , platform , parsed .Host )
75+ // Authentication is optional for public repos on GitLab/Gitea/Codeberg
76+ // Only GitHub strictly requires authentication for most API calls
77+ tokenOptional := parsed .Platform != prx .PlatformGitHub
78+
79+ if err != nil && ! tokenOptional {
80+ return fmt .Errorf ("authentication failed: %w" , err )
7081 }
7182
72- var opts []prx.Option
73- if * debug {
74- opts = append (opts , prx .WithLogger (slog .Default ()))
83+ var tokenValue string
84+ if token != nil {
85+ tokenValue = token .Value
86+ slog .Debug ("Using token" , "source" , token .Source , "host" , token .Host )
87+ } else {
88+ slog .Debug ("No authentication token found, proceeding without authentication" )
7589 }
7690
77- ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Minute )
78- defer cancel ()
91+ // Create platform-specific client
92+ var prxPlatform prx.Platform
93+ switch parsed .Platform {
94+ case prx .PlatformGitHub :
95+ prxPlatform = github .NewPlatform (tokenValue )
96+ case prx .PlatformGitLab :
97+ prxPlatform = gitlab .NewPlatform (tokenValue , gitlab .WithBaseURL ("https://" + parsed .Host ))
98+ case prx .PlatformCodeberg :
99+ prxPlatform = gitea .NewCodebergPlatform (tokenValue )
100+ default :
101+ // Self-hosted Gitea
102+ prxPlatform = gitea .NewPlatform (tokenValue , gitea .WithBaseURL ("https://" + parsed .Host ))
103+ }
79104
80105 // Configure client options
106+ var opts []prx.Option
107+ if * debug {
108+ opts = append (opts , prx .WithLogger (slog .Default ()))
109+ }
81110 if * noCache {
82111 opts = append (opts , prx .WithCacheStore (null .New [string , prx.PullRequestData ]()))
83112 }
84113
85- client := prx .NewClient ( token , opts ... )
86- data , err := client .PullRequestWithReferenceTime (ctx , owner , repo , prNumber , referenceTime )
114+ client := prx .NewClientWithPlatform ( prxPlatform , opts ... )
115+ data , err := client .PullRequestWithReferenceTime (ctx , parsed . Owner , parsed . Repo , parsed . Number , referenceTime )
87116 if err != nil {
88- log .Printf ("Failed to fetch PR data: %v" , err )
89- cancel ()
90- os .Exit (1 ) //nolint:gocritic // False positive: cancel() is called immediately before os.Exit()
117+ return fmt .Errorf ("failed to fetch PR data: %w" , err )
91118 }
92119
93120 encoder := json .NewEncoder (os .Stdout )
94121 if err := encoder .Encode (data ); err != nil {
95- log .Printf ("Failed to encode pull request: %v" , err )
96- cancel ()
97- os .Exit (1 )
98- }
99-
100- cancel () // Ensure context is cancelled before exit
101- }
102-
103- func githubToken () (string , error ) {
104- cmd := exec .CommandContext (context .Background (), "gh" , "auth" , "token" )
105- output , err := cmd .Output ()
106- if err != nil {
107- return "" , fmt .Errorf ("failed to run 'gh auth token': %w" , err )
108- }
109-
110- token := strings .TrimSpace (string (output ))
111- if token == "" {
112- return "" , errors .New ("no token returned by 'gh auth token'" )
122+ return fmt .Errorf ("failed to encode pull request: %w" , err )
113123 }
114124
115- return token , nil
125+ return nil
116126}
117127
118- //nolint:revive // function-result-limit: function needs all 4 return values
119- func parsePRURL (prURL string ) (owner , repo string , prNumber int , err error ) {
120- u , err := url .Parse (prURL )
121- if err != nil {
122- return "" , "" , 0 , err
123- }
124-
125- if u .Host != "github.com" {
126- return "" , "" , 0 , errors .New ("not a GitHub URL" )
127- }
128-
129- parts := strings .Split (strings .Trim (u .Path , "/" ), "/" )
130- if len (parts ) != expectedURLParts || parts [pullPathIndex ] != pullPathValue {
131- return "" , "" , 0 , errors .New ("invalid PR URL format" )
132- }
133-
134- prNumber , err = strconv .Atoi (parts [3 ])
135- if err != nil {
136- return "" , "" , 0 , fmt .Errorf ("invalid PR number: %w" , err )
137- }
138-
139- return parts [0 ], parts [1 ], prNumber , nil
128+ func printUsage () {
129+ fmt .Fprintf (os .Stderr , "Usage: %s [options] <pull-request-url>\n \n " , os .Args [0 ])
130+ fmt .Fprint (os .Stderr , "Supported platforms:\n " )
131+ fmt .Fprint (os .Stderr , " GitHub: https://github.com/owner/repo/pull/123\n " )
132+ fmt .Fprint (os .Stderr , " GitLab: https://gitlab.com/owner/repo/-/merge_requests/123\n " )
133+ fmt .Fprint (os .Stderr , " Codeberg: https://codeberg.org/owner/repo/pulls/123\n \n " )
134+ fmt .Fprint (os .Stderr , "Authentication:\n " )
135+ fmt .Fprint (os .Stderr , " GitHub: GITHUB_TOKEN env or 'gh auth login'\n " )
136+ fmt .Fprint (os .Stderr , " GitLab: GITLAB_TOKEN env or 'glab auth login'\n " )
137+ fmt .Fprint (os .Stderr , " Codeberg: CODEBERG_TOKEN env\n " )
138+ fmt .Fprint (os .Stderr , " Gitea: GITEA_TOKEN env\n \n " )
139+ fmt .Fprint (os .Stderr , "Options:\n " )
140+ flag .PrintDefaults ()
140141}
0 commit comments