From 81fd21ad3d39f6b06f9303d2b04624e51459c1e5 Mon Sep 17 00:00:00 2001 From: Thomas Stromberg Date: Mon, 27 Oct 2025 09:53:10 -0400 Subject: [PATCH 1/3] fix lint errors --- .golangci.yml | 2 - go.mod | 19 +++--- go.sum | 46 +++++-------- main.go | 181 ++++++++++++++++++++++---------------------------- 4 files changed, 106 insertions(+), 142 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 9346056..3e2f4d9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -172,8 +172,6 @@ linters: - name: add-constant severity: warning disabled: true - - name: argument-limit - arguments: [9] - name: cognitive-complexity disabled: true # prefer maintidx - name: cyclomatic diff --git a/go.mod b/go.mod index 1490a83..ff0b15b 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/codeGROOVE-dev/prs go 1.25.1 require ( - github.com/avast/retry-go/v4 v4.6.1 github.com/charmbracelet/lipgloss v1.1.0 - github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001144140-ed20651ca4e9 - github.com/codeGROOVE-dev/turnclient v0.0.0-20251001151440-a58eb9b17826 - golang.org/x/term v0.35.0 + github.com/codeGROOVE-dev/retry v1.2.0 + github.com/codeGROOVE-dev/sprinkler v0.0.0-20251027122631-1d61827b70ca + github.com/codeGROOVE-dev/turnclient v0.0.0-20251022064427-5a712e1e10e6 + golang.org/x/term v0.36.0 ) require ( @@ -16,17 +16,16 @@ require ( github.com/charmbracelet/x/ansi v0.10.2 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/clipperhouse/uax29/v2 v2.2.0 // indirect - github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c // indirect - github.com/codeGROOVE-dev/retry v1.2.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/codeGROOVE-dev/prx v0.0.0-20251027012315-7b273aabfc7d // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect ) diff --git a/go.sum b/go.sum index a89ba8f..9c80c6b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= -github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= @@ -12,24 +10,18 @@ github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= -github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274 h1:9eLzQdOaQEn30279ai3YjNdJOM/efbcYanWC9juAJ+M= -github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w= -github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c h1:/rrjFoqwFqKNzc1f14vQt6QJ9U5tQ4Uh6U8hgixkSqw= -github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/codeGROOVE-dev/prx v0.0.0-20251027012315-7b273aabfc7d h1:kUaCKFRxWFrWEyl4fVHi+eY/D5tKhBU29a8YbQyihEk= +github.com/codeGROOVE-dev/prx v0.0.0-20251027012315-7b273aabfc7d/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w= github.com/codeGROOVE-dev/retry v1.2.0 h1:xYpYPX2PQZmdHwuiQAGGzsBm392xIMl4nfMEFApQnu8= github.com/codeGROOVE-dev/retry v1.2.0/go.mod h1:8OgefgV1XP7lzX2PdKlCXILsYKuz6b4ZpHa/20iLi8E= -github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001125233-5fa6f0ff4582 h1:IPCaNGRWdyMZKyjnjv+wdSmPmOZtKFD6SVaha5DuCqk= -github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001125233-5fa6f0ff4582/go.mod h1:RZ/Te7HkY5upHQlnmf3kV4GHVM0R8AK3U+yPItCZAoQ= -github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001144140-ed20651ca4e9 h1:Nuyy0vMl6YD96N6WwZeqClPa/VlaILJYZoG50ezOAHw= -github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001144140-ed20651ca4e9/go.mod h1:RZ/Te7HkY5upHQlnmf3kV4GHVM0R8AK3U+yPItCZAoQ= -github.com/codeGROOVE-dev/turnclient v0.0.0-20250929203714-61cf2f094fb1 h1:lQZoQN9Vo+AzGHGRMAoFewJ07vS24cNIEx2GrL5FX/g= -github.com/codeGROOVE-dev/turnclient v0.0.0-20250929203714-61cf2f094fb1/go.mod h1:7lBF4vS6T+D1rNjmJ+CNVrXALQvdwNfBVEy7vhIQtYk= -github.com/codeGROOVE-dev/turnclient v0.0.0-20251001151440-a58eb9b17826 h1:ly6n4spiC6r0IOMl8QfZjv+qUnMHLvo/qErGPVMV3IE= -github.com/codeGROOVE-dev/turnclient v0.0.0-20251001151440-a58eb9b17826/go.mod h1:JXk9gT6Qb496lnTcgpk9h917XaREGa+t6Kvg0YHAQJY= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251027122631-1d61827b70ca h1:NDBJTf69PxMsZkZLUjvnfiMQHWL6Y2T4jQT5YNzXTXA= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251027122631-1d61827b70ca/go.mod h1:/kd3ncsRNldD0MUpbtp5ojIzfCkyeXB7JdOrpuqG7Gg= +github.com/codeGROOVE-dev/turnclient v0.0.0-20251022064427-5a712e1e10e6 h1:7FCmaftkl362oTZHVJyUg+xhxqfQFx+JisBf7RgklL8= +github.com/codeGROOVE-dev/turnclient v0.0.0-20251022064427-5a712e1e10e6/go.mod h1:fYwtN9Ql6lY8t2WvCfENx+mP5FUwjlqwXCLx9CVLY20= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -38,22 +30,16 @@ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byF github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= diff --git a/main.go b/main.go index cb71e1e..15cd4fb 100644 --- a/main.go +++ b/main.go @@ -18,14 +18,15 @@ import ( "os/exec" "os/signal" "path/filepath" + "sort" "strconv" "strings" "sync" "syscall" "time" - "github.com/avast/retry-go/v4" "github.com/charmbracelet/lipgloss" + "github.com/codeGROOVE-dev/retry" "github.com/codeGROOVE-dev/sprinkler/pkg/client" "github.com/codeGROOVE-dev/turnclient/pkg/turn" "golang.org/x/term" @@ -360,14 +361,12 @@ func gitHubToken(ctx context.Context) (string, error) { } func currentUser(ctx context.Context, token string, logger *log.Logger, httpClient *http.Client) (string, error) { - var username string - - err := retry.Do( - func() error { + return retry.DoWithData( + func() (string, error) { logger.Printf("Making API call to GET %s", apiUserEndpoint) req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiUserEndpoint, http.NoBody) if err != nil { - return err + return "", err } req.Header.Set("Authorization", "token "+token) @@ -377,30 +376,29 @@ func currentUser(ctx context.Context, token string, logger *log.Logger, httpClie resp, err := httpClient.Do(req) if err != nil { logger.Printf("HTTP request failed: %v", err) - return err + return "", err } defer resp.Body.Close() //nolint:errcheck // Best effort close if resp.StatusCode == http.StatusUnauthorized { - return errors.New("invalid GitHub token") + return "", errors.New("invalid GitHub token") } if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return fmt.Errorf("failed to read response body: %w", err) + return "", fmt.Errorf("failed to read response body: %w", err) } - return fmt.Errorf("GitHub API error (status %d): %s", resp.StatusCode, body) + return "", fmt.Errorf("GitHub API error (status %d): %s", resp.StatusCode, body) } var user struct { Login string `json:"login"` } if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { - return err + return "", err } - username = user.Login - return nil + return user.Login, nil }, retry.Attempts(retryAttempts), retry.Delay(retryDelay), @@ -411,27 +409,18 @@ func currentUser(ctx context.Context, token string, logger *log.Logger, httpClie logger.Printf("Retry attempt %d after error: %v", n+1, err) }), ) - - return username, err } func fetchPRsWithRetry(ctx context.Context, query *prQuery, logger *log.Logger, cls *clients) ([]PR, error) { - var prs []PR - - err := retry.Do( - func() error { + return retry.DoWithData( + func() ([]PR, error) { select { case <-ctx.Done(): - return ctx.Err() + return nil, ctx.Err() default: } - result, err := fetchPRs(ctx, query, logger, cls) - if err != nil { - return err - } - prs = result - return nil + return fetchPRs(ctx, query, logger, cls) }, retry.Context(ctx), retry.Attempts(retryAttempts), @@ -443,8 +432,6 @@ func fetchPRsWithRetry(ctx context.Context, query *prQuery, logger *log.Logger, logger.Printf("Retry attempt %d for fetchPRs: %v", n+1, err) }), ) - - return prs, err } func fetchPRs(ctx context.Context, query *prQuery, logger *log.Logger, cls *clients) ([]PR, error) { @@ -493,7 +480,16 @@ func fetchPRs(ctx context.Context, query *prQuery, logger *log.Logger, cls *clie prs = deduplicatePRs(prs) logger.Printf("Found %d PRs (after deduplication)", len(prs)) - enrichPRsParallel(ctx, query.token, prs, logger, cls.http, cls.turn, query.username, query.debug, query.noCache) + enrichCfg := &enrichConfig{ + httpClient: cls.http, + turnClient: cls.turn, + logger: logger, + token: query.token, + username: query.username, + debug: query.debug, + noCache: query.noCache, + } + enrichPRsParallel(ctx, prs, enrichCfg) logger.Printf("INFO: Successfully enriched all %d PRs", len(prs)) // Filter out closed/merged PRs (can happen due to GitHub search lag or stale cache) @@ -569,10 +565,6 @@ func parseSearchResponse(resp *http.Response) ([]PR, error) { } func deduplicatePRs(prs []PR) []PR { - if len(prs) <= 1 { - return prs - } - seen := make(map[string]PR, len(prs)) for i := range prs { @@ -582,16 +574,14 @@ func deduplicatePRs(prs []PR) []PR { } result := make([]PR, 0, len(seen)) - for k := range seen { - result = append(result, seen[k]) + for u := range seen { + result = append(result, seen[u]) } return result } -func enrichPRsParallel(ctx context.Context, token string, prs []PR, logger *log.Logger, - httpClient *http.Client, turnClient *turn.Client, username string, debug bool, noCache bool, -) { +func enrichPRsParallel(ctx context.Context, prs []PR, cfg *enrichConfig) { // Simple semaphore pattern - Rob Pike style sem := make(chan struct{}, maxConcurrent) var wg sync.WaitGroup @@ -606,11 +596,11 @@ func enrichPRsParallel(ctx context.Context, token string, prs []PR, logger *log. }() // Ignore non-critical errors - let the app continue - if err := enrichPRData(ctx, pr, token, logger, httpClient, turnClient, username, debug, noCache); err != nil { + if err := enrichPRData(ctx, pr, cfg); err != nil { if errors.Is(err, context.Canceled) { return } - logger.Printf("WARNING: Failed to enrich PR #%d: %v", pr.Number, err) + cfg.logger.Printf("WARNING: Failed to enrich PR #%d: %v", pr.Number, err) } }) } @@ -669,92 +659,80 @@ func fetchPRDetails(ctx context.Context, pr *PR, token string, httpClient *http. return nil } -func enrichPRData( - ctx context.Context, - pr *PR, - token string, - logger *log.Logger, - httpClient *http.Client, - turnClient *turn.Client, - username string, - debug bool, - noCache bool, -) error { +func enrichPRData(ctx context.Context, pr *PR, cfg *enrichConfig) error { start := time.Now() defer func() { - if debug { - logger.Printf("Enriched PR #%d in %v", pr.Number, time.Since(start)) + if cfg.debug { + cfg.logger.Printf("Enriched PR #%d in %v", pr.Number, time.Since(start)) } }() // Fetch individual PR data to get size information - if err := fetchPRDetails(ctx, pr, token, httpClient, logger, debug); err != nil { - logger.Printf("WARNING: Failed to fetch PR details for #%d: %v", pr.Number, err) + if err := fetchPRDetails(ctx, pr, cfg.token, cfg.httpClient, cfg.logger, cfg.debug); err != nil { + cfg.logger.Printf("WARNING: Failed to fetch PR details for #%d: %v", pr.Number, err) // Continue without size info } // Enrich with turn server data if available - if turnClient == nil { - if debug { - logger.Printf("Turn client is nil, skipping turn enrichment for PR #%d", pr.Number) + if cfg.turnClient == nil { + if cfg.debug { + cfg.logger.Printf("Turn client is nil, skipping turn enrichment for PR #%d", pr.Number) } return nil } - if debug { - logger.Printf("Calling enrichWithTurnData for PR #%d", pr.Number) + if cfg.debug { + cfg.logger.Printf("Calling enrichWithTurnData for PR #%d", pr.Number) } - return enrichWithTurnData(ctx, pr, logger, turnClient, username, debug, noCache) + return enrichWithTurnData(ctx, pr, cfg) } -func enrichWithTurnData(ctx context.Context, pr *PR, logger *log.Logger, turnClient *turn.Client, username string, debug bool, noCache bool) error { - if debug { - logger.Printf("enrichWithTurnData called for PR #%d, URL: %s", pr.Number, pr.HTMLURL) +func enrichWithTurnData(ctx context.Context, pr *PR, cfg *enrichConfig) error { + if cfg.debug { + cfg.logger.Printf("enrichWithTurnData called for PR #%d, URL: %s", pr.Number, pr.HTMLURL) } // Validate PR URL before sending to turn server if pr.HTMLURL == "" || !strings.HasPrefix(pr.HTMLURL, "https://github.com/") { - logger.Printf("WARNING: Invalid PR URL for turn enrichment: %s", pr.HTMLURL) + cfg.logger.Printf("WARNING: Invalid PR URL for turn enrichment: %s", pr.HTMLURL) return nil } // Check cache first (unless noCache is enabled) var cachePath string - if !noCache { + if !cfg.noCache { cachePath = turnCachePath(pr.HTMLURL, pr.UpdatedAt) - if debug { - logger.Printf("Cache path for PR #%d: %s", pr.Number, cachePath) + if cfg.debug { + cfg.logger.Printf("Cache path for PR #%d: %s", pr.Number, cachePath) } if cached, found := loadTurnCache(cachePath); found { - if debug { - logger.Printf("INFO: Cache hit for PR #%d", pr.Number) + if cfg.debug { + cfg.logger.Printf("INFO: Cache hit for PR #%d", pr.Number) } pr.TurnResponse = cached return nil } // Cache miss - if debug { - logger.Printf("INFO: Cache miss for PR #%d", pr.Number) + if cfg.debug { + cfg.logger.Printf("INFO: Cache miss for PR #%d", pr.Number) } - } else if debug { - logger.Printf("INFO: Cache disabled (--no-cache) for PR #%d", pr.Number) + } else if cfg.debug { + cfg.logger.Printf("INFO: Cache disabled (--no-cache) for PR #%d", pr.Number) } - return fetchAndCacheTurnData(ctx, pr, logger, turnClient, username, debug, cachePath, noCache) + return fetchAndCacheTurnData(ctx, pr, cachePath, cfg) } -func fetchAndCacheTurnData(ctx context.Context, pr *PR, logger *log.Logger, - turnClient *turn.Client, username string, debug bool, cachePath string, noCache bool, -) error { +func fetchAndCacheTurnData(ctx context.Context, pr *PR, cachePath string, cfg *enrichConfig) error { turnStart := time.Now() - if debug { - logger.Printf("Sending turnclient request for PR #%d: URL=%s, UpdatedAt=%s", + if cfg.debug { + cfg.logger.Printf("Sending turnclient request for PR #%d: URL=%s, UpdatedAt=%s", pr.Number, pr.HTMLURL, pr.UpdatedAt.Format(time.RFC3339)) } - turnResponse, err := turnClient.Check(ctx, pr.HTMLURL, username, pr.UpdatedAt) + turnResponse, err := cfg.turnClient.Check(ctx, pr.HTMLURL, cfg.username, pr.UpdatedAt) if err != nil { - logger.Printf("WARNING: Failed to get turn data for PR #%d: %v", pr.Number, err) + cfg.logger.Printf("WARNING: Failed to get turn data for PR #%d: %v", pr.Number, err) return nil // Don't fail the entire enrichment if turn server is unavailable } @@ -763,30 +741,27 @@ func fetchAndCacheTurnData(ctx context.Context, pr *PR, logger *log.Logger, } pr.TurnResponse = turnResponse - if !noCache { + if !cfg.noCache { saveTurnCache(cachePath, turnResponse) } - if debug { - if err := logDebugTurnResponse(logger, pr.Number, turnResponse, time.Since(turnStart)); err != nil { - return err - } + if cfg.debug { + logDebugTurnResponse(cfg.logger, pr.Number, turnResponse, time.Since(turnStart)) } return nil } -func logDebugTurnResponse(logger *log.Logger, prNumber int, turnResponse *turn.CheckResponse, duration time.Duration) error { +func logDebugTurnResponse(logger *log.Logger, prNumber int, turnResponse *turn.CheckResponse, duration time.Duration) { logger.Printf("Turn server call for PR #%d took %v", prNumber, duration) responseJSON, err := json.MarshalIndent(turnResponse, "", " ") if err != nil { - logger.Printf("ERROR: Failed to marshal turn response for PR #%d: %v", prNumber, err) + logger.Printf("WARNING: Failed to marshal turn response for PR #%d: %v", prNumber, err) // Try to at least log some basic info logger.Printf("Turn response for PR #%d: Analysis.Tags=%v, NextActions=%d", prNumber, turnResponse.Analysis.Tags, len(turnResponse.Analysis.NextAction)) - return fmt.Errorf("failed to marshal turn response: %w", err) + return } logger.Printf("Received turnclient response for PR #%d:\n%s", prNumber, string(responseJSON)) - return nil } func isBlockingOnUser(pr *PR, username string) bool { @@ -855,6 +830,17 @@ type clients struct { turn *turn.Client } +// enrichConfig holds configuration for PR enrichment. +type enrichConfig struct { + httpClient *http.Client + turnClient *turn.Client + logger *log.Logger + token string + username string + debug bool + noCache bool +} + // prQuery holds parameters for PR queries. type prQuery struct { token string @@ -907,9 +893,8 @@ func runWatchMode(ctx context.Context, cfg *watchConfig) { // Create refresh tracker to prevent duplicate API calls refreshTracker := newPRRefreshTracker() - const updateChanSize = 10 // Channel to trigger display updates - updateChan := make(chan bool, updateChanSize) + updateChan := make(chan bool, 10) // Initial display displayCfg := &displayConfig{ @@ -1390,11 +1375,7 @@ func orgFromURL(urlStr string) string { } func sortPRsByUpdateTime(prs []PR) { - for i := 0; i < len(prs); i++ { - for j := i + 1; j < len(prs); j++ { - if prs[j].UpdatedAt.After(prs[i].UpdatedAt) { - prs[i], prs[j] = prs[j], prs[i] - } - } - } + sort.Slice(prs, func(i, j int) bool { + return prs[i].UpdatedAt.After(prs[j].UpdatedAt) + }) } From 062e8032305b5f9d66b996f9cc635e452c4cb7f7 Mon Sep 17 00:00:00 2001 From: Thomas Stromberg Date: Sat, 17 Jan 2026 11:29:22 -0500 Subject: [PATCH 2/3] upgrade go deps, migrate cache to fido --- .golangci.yml | 2 + Makefile | 2 +- README.md | 6 ++ go.mod | 30 +++--- go.sum | 60 +++++++---- main.go | 271 ++++++++++++++++++-------------------------------- 6 files changed, 160 insertions(+), 211 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 3e2f4d9..b455b96 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -186,6 +186,8 @@ linters: arguments: [10] - name: flag-parameter # fixes are difficult disabled: true + - name: bare-return + disabled: true rowserrcheck: # database/sql is always checked. diff --git a/Makefile b/Makefile index b090010..a8f343e 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ LINTERS := FIXERS := GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml -GOLANGCI_LINT_VERSION ?= v2.5.0 +GOLANGCI_LINT_VERSION ?= v2.7.2 GOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH) $(GOLANGCI_LINT_BIN): mkdir -p $(LINT_ROOT)/out/linters diff --git a/README.md b/README.md index 57a7c3d..b12a004 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ Designed to be easily used for embedded low-power displays, or your shell initia ## Install +Via Homebrew: +```bash +brew install codegroove-dev/tap/review-goose-cli +``` + +Or via Go: ```bash go install github.com/codeGROOVE-dev/prs@latest ``` diff --git a/go.mod b/go.mod index ff0b15b..0ba0512 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,37 @@ module github.com/codeGROOVE-dev/prs -go 1.25.1 +go 1.25.4 require ( github.com/charmbracelet/lipgloss v1.1.0 - github.com/codeGROOVE-dev/retry v1.2.0 - github.com/codeGROOVE-dev/sprinkler v0.0.0-20251027122631-1d61827b70ca - github.com/codeGROOVE-dev/turnclient v0.0.0-20251022064427-5a712e1e10e6 - golang.org/x/term v0.36.0 + github.com/codeGROOVE-dev/retry v1.3.1 + github.com/codeGROOVE-dev/sprinkler v0.0.0-20260117025717-3985b18e658a + github.com/codeGROOVE-dev/turnclient v0.0.0-20260116165138-9bd9013c5156 + golang.org/x/term v0.39.0 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.3.2 // indirect - github.com/charmbracelet/x/ansi v0.10.2 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/x/ansi v0.11.4 // indirect + github.com/charmbracelet/x/cellbuf v0.0.14 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.7.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.3.0 // indirect - github.com/codeGROOVE-dev/prx v0.0.0-20251027012315-7b273aabfc7d // indirect + github.com/codeGROOVE-dev/fido v1.10.0 // indirect + github.com/codeGROOVE-dev/fido/pkg/store/compress v1.10.0 // indirect + github.com/codeGROOVE-dev/fido/pkg/store/localfs v1.10.0 // indirect + github.com/codeGROOVE-dev/prx v0.0.0-20260116145942-52ee64398c48 // indirect + github.com/klauspost/compress v1.18.3 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/puzpuzpuz/xsync/v4 v4.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/sys v0.37.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index 9c80c6b..ccff99a 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,37 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= -github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= +github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= +github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= -github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI= +github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4= +github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= +github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE= +github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/codeGROOVE-dev/prx v0.0.0-20251027012315-7b273aabfc7d h1:kUaCKFRxWFrWEyl4fVHi+eY/D5tKhBU29a8YbQyihEk= -github.com/codeGROOVE-dev/prx v0.0.0-20251027012315-7b273aabfc7d/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w= -github.com/codeGROOVE-dev/retry v1.2.0 h1:xYpYPX2PQZmdHwuiQAGGzsBm392xIMl4nfMEFApQnu8= -github.com/codeGROOVE-dev/retry v1.2.0/go.mod h1:8OgefgV1XP7lzX2PdKlCXILsYKuz6b4ZpHa/20iLi8E= -github.com/codeGROOVE-dev/sprinkler v0.0.0-20251027122631-1d61827b70ca h1:NDBJTf69PxMsZkZLUjvnfiMQHWL6Y2T4jQT5YNzXTXA= -github.com/codeGROOVE-dev/sprinkler v0.0.0-20251027122631-1d61827b70ca/go.mod h1:/kd3ncsRNldD0MUpbtp5ojIzfCkyeXB7JdOrpuqG7Gg= -github.com/codeGROOVE-dev/turnclient v0.0.0-20251022064427-5a712e1e10e6 h1:7FCmaftkl362oTZHVJyUg+xhxqfQFx+JisBf7RgklL8= -github.com/codeGROOVE-dev/turnclient v0.0.0-20251022064427-5a712e1e10e6/go.mod h1:fYwtN9Ql6lY8t2WvCfENx+mP5FUwjlqwXCLx9CVLY20= +github.com/codeGROOVE-dev/fido v1.10.0 h1:i4Wb6LDd5nD/4Fnp47KAVUVhG1O1mN5jSRbCYPpBYjw= +github.com/codeGROOVE-dev/fido v1.10.0/go.mod h1:/mqfMeKCTYTGt/Y0cWm6gh8gYBKG1w8xBsTDmu+A/pU= +github.com/codeGROOVE-dev/fido/pkg/store/compress v1.10.0 h1:W3AYtR6eyPHQ8QhTsuqjNZYWk/Fev0cJiAiuw04uhlk= +github.com/codeGROOVE-dev/fido/pkg/store/compress v1.10.0/go.mod h1:0hFYQ8Y6jfrYuJb8eBimYz66tg7DDuVWbZqaI944LQM= +github.com/codeGROOVE-dev/fido/pkg/store/localfs v1.10.0 h1:oaPwuHHBuzhsWnPm7UCxgwjz7+jG3O0JenSSgPSwqv8= +github.com/codeGROOVE-dev/fido/pkg/store/localfs v1.10.0/go.mod h1:zUGzODSWykosAod0IHycxdxUOMcd2eVqd6eUdOsU73E= +github.com/codeGROOVE-dev/prx v0.0.0-20260116145942-52ee64398c48 h1:VZQpJ0R8LGGAIC8G2gVzzwytAbK01IkjgD7fKVNQxH0= +github.com/codeGROOVE-dev/prx v0.0.0-20260116145942-52ee64398c48/go.mod h1:eiIA1uMUCthQAeNEIa6KQeLCn23WbjOWt3bvxtxzgOw= +github.com/codeGROOVE-dev/retry v1.3.1 h1:BAkfDzs6FssxLCGWGgM97bb+6/8GTa40Cs147vXkJOg= +github.com/codeGROOVE-dev/retry v1.3.1/go.mod h1:+b3huqYGY1+ZJyuCmR8nBVLjd3WJ7qAFss+sI4s6FSc= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20260117025717-3985b18e658a h1:W13W4gtRwD409ayQF4c6gNZwvfuYowIb8JKyd4bgmDU= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20260117025717-3985b18e658a/go.mod h1:SBz4HTjsHOC2cUL5uHeaMt5nvyse1Ft56K3vxpoZuos= +github.com/codeGROOVE-dev/turnclient v0.0.0-20260116165138-9bd9013c5156 h1:a8BmAWU9UKHFghter0MY/pNXLLCsvTkKC7Vf8zhs+BI= +github.com/codeGROOVE-dev/turnclient v0.0.0-20260116165138-9bd9013c5156/go.mod h1:dQAoztfqCHl0X8VbxMEAfcrVXQaHirDVLBr9xwvGA9c= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -30,16 +40,22 @@ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byF github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +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.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q= +github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 15cd4fb..9bd64ac 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ package main import ( "context" "crypto/sha256" - "encoding/hex" "encoding/json" "errors" "flag" @@ -17,7 +16,7 @@ import ( "os" "os/exec" "os/signal" - "path/filepath" + "slices" "sort" "strconv" "strings" @@ -26,6 +25,7 @@ import ( "time" "github.com/charmbracelet/lipgloss" + "github.com/codeGROOVE-dev/fido/pkg/store/localfs" "github.com/codeGROOVE-dev/retry" "github.com/codeGROOVE-dev/sprinkler/pkg/client" "github.com/codeGROOVE-dev/turnclient/pkg/turn" @@ -70,28 +70,16 @@ type SearchResult struct { Items []PR `json:"items"` } -// cacheEntry represents a cached Turn API response. -type cacheEntry struct { - Response *turn.CheckResponse `json:"response"` - Timestamp time.Time `json:"timestamp"` -} - // prRefreshTracker tracks the last refresh time for PRs to avoid duplicate API calls. type prRefreshTracker struct { lastRefresh map[string]time.Time mu sync.RWMutex } -func newPRRefreshTracker() *prRefreshTracker { - return &prRefreshTracker{ - lastRefresh: make(map[string]time.Time), - } -} - -func (t *prRefreshTracker) shouldRefresh(prURL string) bool { - t.mu.RLock() - lastTime, exists := t.lastRefresh[prURL] - t.mu.RUnlock() +func (r *prRefreshTracker) shouldRefresh(prURL string) bool { + r.mu.RLock() + lastTime, exists := r.lastRefresh[prURL] + r.mu.RUnlock() if !exists { return true @@ -100,10 +88,10 @@ func (t *prRefreshTracker) shouldRefresh(prURL string) bool { return time.Since(lastTime) > time.Duration(prRefreshCooldownSecs)*time.Second } -func (t *prRefreshTracker) markRefreshed(prURL string) { - t.mu.Lock() - t.lastRefresh[prURL] = time.Now() - t.mu.Unlock() +func (r *prRefreshTracker) markRefreshed(prURL string) { + r.mu.Lock() + r.lastRefresh[prURL] = time.Now() + r.mu.Unlock() } const ( @@ -126,66 +114,19 @@ const ( minTokenLength = 10 // Minimum GitHub token length maxIdleConnsPerHost = 10 // HTTP client setting idleConnTimeout = 90 * time.Second - minPRURLParts = 6 // Minimum parts in PR URL - minOrgURLParts = 4 // Minimum parts in org URL - repoPartIndex = 4 // Index of repo in URL parts - prTypePartIndex = 5 // Index of PR type in URL parts - numberPartIndex = 6 // Index of PR number in URL parts - prURLParts = 7 // Number of parts in a full PR URL - truncatedURLLength = 80 // Max URL display length - defaultTerminalWidth = 80 // Default terminal width if detection fails - titlePadding = 5 // Space between title and URL - minTitleLength = 20 // Minimum title display length - cacheFileMode = 0o644 // File permissions for cache files - stalePRDays = 90 // Days before a PR is considered stale + minPRURLParts = 6 // Minimum parts in PR URL + minOrgURLParts = 4 // Minimum parts in org URL + repoPartIndex = 4 // Index of repo in URL parts + prTypePartIndex = 5 // Index of PR type in URL parts + numberPartIndex = 6 // Index of PR number in URL parts + prURLParts = 7 // Number of parts in a full PR URL + truncatedURLLength = 80 // Max URL display length + defaultTerminalWidth = 80 // Default terminal width if detection fails + titlePadding = 5 // Space between title and URL + minTitleLength = 20 // Minimum title display length + stalePRDays = 90 // Days before a PR is considered stale ) -// turnCache handles caching of Turn API responses. -func turnCachePath(urlPath string, updatedAt time.Time) string { - dir, err := os.UserCacheDir() - if err != nil || dir == "" { - return "" // No cache if we can't find cache dir - } - - // Simple hash for filename - h := sha256.Sum256([]byte(urlPath + updatedAt.Format(time.RFC3339))) - return filepath.Join(dir, "prs", "turn-cache", hex.EncodeToString(h[:8])+".json") -} - -func loadTurnCache(path string) (*turn.CheckResponse, bool) { - data, err := os.ReadFile(path) - if err != nil { - return nil, false - } - - var entry cacheEntry - if json.Unmarshal(data, &entry) != nil { - return nil, false - } - - // Check if expired - if time.Since(entry.Timestamp) > cacheTTL { - // Best effort removal of expired cache - os.Remove(path) //nolint:errcheck,gosec // Removal failures are acceptable - return nil, false - } - - return entry.Response, true -} - -func saveTurnCache(path string, response *turn.CheckResponse) { - if path == "" { - return - } - - // Best effort cache write - failures are non-critical - os.MkdirAll(filepath.Dir(path), 0o755) //nolint:errcheck,gosec // Directory creation failures are acceptable - data, err := json.Marshal(cacheEntry{Response: response, Timestamp: time.Now()}) - if err == nil { - os.WriteFile(path, data, cacheFileMode) //nolint:errcheck,gosec // Write failures are acceptable - } -} - func main() { var ( watch = flag.Bool("watch", false, "Continuously watch for PR updates") @@ -267,6 +208,13 @@ func main() { } } + // Set up cache store + cache, err := localfs.New[string, *turn.CheckResponse]("prs-turn-cache", "") + if err != nil { + logger.Printf("WARNING: Failed to create cache store: %v", err) + cache = nil + } + // Handle interrupts gracefully sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) @@ -297,6 +245,7 @@ func main() { logger: logger, httpClient: httpClient, turnClient: turnClient, + cache: cache, debug: *verbose, org: "", includeStale: *includeStale, @@ -314,8 +263,9 @@ func main() { noCache: *noCache, } cls := &clients{ - http: httpClient, - turn: turnClient, + http: httpClient, + turn: turnClient, + cache: cache, } prs, err := fetchPRsWithRetry(ctx, query, logger, cls) if err != nil { @@ -483,6 +433,7 @@ func fetchPRs(ctx context.Context, query *prQuery, logger *log.Logger, cls *clie enrichCfg := &enrichConfig{ httpClient: cls.http, turnClient: cls.turn, + cache: cls.cache, logger: logger, token: query.token, username: query.username, @@ -697,33 +648,32 @@ func enrichWithTurnData(ctx context.Context, pr *PR, cfg *enrichConfig) error { return nil } - // Check cache first (unless noCache is enabled) - var cachePath string - if !cfg.noCache { - cachePath = turnCachePath(pr.HTMLURL, pr.UpdatedAt) - if cfg.debug { - cfg.logger.Printf("Cache path for PR #%d: %s", pr.Number, cachePath) - } - if cached, found := loadTurnCache(cachePath); found { + // Check cache first (unless noCache is enabled or cache is unavailable) + cacheKey := pr.HTMLURL + ":" + pr.UpdatedAt.Format(time.RFC3339) + if !cfg.noCache && cfg.cache != nil { + cached, _, found, err := cfg.cache.Get(ctx, cacheKey) + if err == nil && found { if cfg.debug { cfg.logger.Printf("INFO: Cache hit for PR #%d", pr.Number) } pr.TurnResponse = cached return nil } - - // Cache miss if cfg.debug { cfg.logger.Printf("INFO: Cache miss for PR #%d", pr.Number) } } else if cfg.debug { - cfg.logger.Printf("INFO: Cache disabled (--no-cache) for PR #%d", pr.Number) + if cfg.noCache { + cfg.logger.Printf("INFO: Cache disabled (--no-cache) for PR #%d", pr.Number) + } else { + cfg.logger.Printf("INFO: Cache unavailable for PR #%d", pr.Number) + } } - return fetchAndCacheTurnData(ctx, pr, cachePath, cfg) + return fetchAndCacheTurnData(ctx, pr, cacheKey, cfg) } -func fetchAndCacheTurnData(ctx context.Context, pr *PR, cachePath string, cfg *enrichConfig) error { +func fetchAndCacheTurnData(ctx context.Context, pr *PR, cacheKey string, cfg *enrichConfig) error { turnStart := time.Now() if cfg.debug { cfg.logger.Printf("Sending turnclient request for PR #%d: URL=%s, UpdatedAt=%s", @@ -733,7 +683,7 @@ func fetchAndCacheTurnData(ctx context.Context, pr *PR, cachePath string, cfg *e turnResponse, err := cfg.turnClient.Check(ctx, pr.HTMLURL, cfg.username, pr.UpdatedAt) if err != nil { cfg.logger.Printf("WARNING: Failed to get turn data for PR #%d: %v", pr.Number, err) - return nil // Don't fail the entire enrichment if turn server is unavailable + return nil } if turnResponse == nil { @@ -741,29 +691,27 @@ func fetchAndCacheTurnData(ctx context.Context, pr *PR, cachePath string, cfg *e } pr.TurnResponse = turnResponse - if !cfg.noCache { - saveTurnCache(cachePath, turnResponse) + if !cfg.noCache && cfg.cache != nil { + expiry := time.Now().Add(cacheTTL) + if err := cfg.cache.Set(ctx, cacheKey, turnResponse, expiry); err != nil && cfg.debug { + cfg.logger.Printf("WARNING: Failed to cache turn data for PR #%d: %v", pr.Number, err) + } } if cfg.debug { - logDebugTurnResponse(cfg.logger, pr.Number, turnResponse, time.Since(turnStart)) + cfg.logger.Printf("Turn server call for PR #%d took %v", pr.Number, time.Since(turnStart)) + responseJSON, err := json.MarshalIndent(turnResponse, "", " ") + if err != nil { + cfg.logger.Printf("WARNING: Failed to marshal turn response for PR #%d: %v", pr.Number, err) + cfg.logger.Printf("Turn response for PR #%d: Analysis.Tags=%v, NextActions=%d", + pr.Number, turnResponse.Analysis.Tags, len(turnResponse.Analysis.NextAction)) + } else { + cfg.logger.Printf("Received turnclient response for PR #%d:\n%s", pr.Number, string(responseJSON)) + } } return nil } -func logDebugTurnResponse(logger *log.Logger, prNumber int, turnResponse *turn.CheckResponse, duration time.Duration) { - logger.Printf("Turn server call for PR #%d took %v", prNumber, duration) - responseJSON, err := json.MarshalIndent(turnResponse, "", " ") - if err != nil { - logger.Printf("WARNING: Failed to marshal turn response for PR #%d: %v", prNumber, err) - // Try to at least log some basic info - logger.Printf("Turn response for PR #%d: Analysis.Tags=%v, NextActions=%d", - prNumber, turnResponse.Analysis.Tags, len(turnResponse.Analysis.NextAction)) - return - } - logger.Printf("Received turnclient response for PR #%d:\n%s", prNumber, string(responseJSON)) -} - func isBlockingOnUser(pr *PR, username string) bool { // If we have turn client data, use that for blocking determination if pr.TurnResponse != nil && pr.TurnResponse.Analysis.NextAction != nil { @@ -801,6 +749,15 @@ func categorizePRs(prs []PR, username string) (incoming, outgoing []PR) { return incoming, outgoing } +func wasBlockingBefore(pr *PR, previous []PR, username string) bool { + for i := range previous { + if previous[i].Number == pr.Number && previous[i].Repository.FullName == pr.Repository.FullName { + return isBlockingOnUser(&previous[i], username) + } + } + return false +} + func truncate(s string, n int) string { if len(s) <= n { return s @@ -808,32 +765,26 @@ func truncate(s string, n int) string { return s[:n-3] + "..." } -func wasBlockingBefore(pr *PR, previous []PR, username string) bool { - if found, exists := findPRInList(pr, previous); exists { - return isBlockingOnUser(&found, username) - } - return false -} - -func findPRInList(target *PR, prs []PR) (PR, bool) { - for i := range prs { - if prs[i].Number == target.Number && prs[i].Repository.FullName == target.Repository.FullName { - return prs[i], true - } +func orgFromURL(urlStr string) string { + parts := strings.Split(urlStr, "/") + if len(parts) >= minOrgURLParts && strings.Contains(urlStr, "github.com") { + return parts[3] } - return PR{}, false + return "" } // clients holds HTTP and Turn clients. type clients struct { - http *http.Client - turn *turn.Client + http *http.Client + turn *turn.Client + cache *localfs.Store[string, *turn.CheckResponse] } // enrichConfig holds configuration for PR enrichment. type enrichConfig struct { httpClient *http.Client turnClient *turn.Client + cache *localfs.Store[string, *turn.CheckResponse] logger *log.Logger token string username string @@ -855,6 +806,7 @@ type displayConfig struct { logger *log.Logger httpClient *http.Client turnClient *turn.Client + cache *localfs.Store[string, *turn.CheckResponse] lastDisplayHash *string token string username string @@ -870,6 +822,7 @@ type displayConfig struct { type watchConfig struct { httpClient *http.Client turnClient *turn.Client + cache *localfs.Store[string, *turn.CheckResponse] logger *log.Logger org string token string @@ -891,7 +844,9 @@ func runWatchMode(ctx context.Context, cfg *watchConfig) { var lastDisplayHash string // Create refresh tracker to prevent duplicate API calls - refreshTracker := newPRRefreshTracker() + refreshTracker := &prRefreshTracker{ + lastRefresh: make(map[string]time.Time), + } // Channel to trigger display updates updateChan := make(chan bool, 10) @@ -901,6 +856,7 @@ func runWatchMode(ctx context.Context, cfg *watchConfig) { logger: cfg.logger, httpClient: cfg.httpClient, turnClient: cfg.turnClient, + cache: cfg.cache, lastDisplayHash: &lastDisplayHash, excludedOrgs: cfg.excludedOrgs, token: cfg.token, @@ -1017,8 +973,9 @@ func updateDisplay(ctx context.Context, cfg *displayConfig) error { noCache: cfg.noCache, } cls := &clients{ - http: cfg.httpClient, - turn: cfg.turnClient, + http: cfg.httpClient, + turn: cfg.turnClient, + cache: cfg.cache, } prs, err := fetchPRsWithRetry(ctx, query, cfg.logger, cls) if err != nil { @@ -1051,15 +1008,8 @@ func filterExcludedOrgs(prs []PR, excludedOrgs []string) []PR { filtered := make([]PR, 0, len(prs)) for i := range prs { - excluded := false org := orgFromURL(prs[i].HTMLURL) - for _, exc := range excludedOrgs { - if org == exc { - excluded = true - break - } - } - if !excluded { + if !slices.Contains(excludedOrgs, org) { filtered = append(filtered, prs[i]) } } @@ -1071,24 +1021,9 @@ func filterStalePRs(prs []PR) []PR { filtered := make([]PR, 0, len(prs)) staleAge := stalePRDays * 24 * time.Hour for i := range prs { - stale := false - - // Check if PR is older than 90 days based on UpdatedAt - if time.Since(prs[i].UpdatedAt) > staleAge { - stale = true - } - - // Also check TurnResponse tags if available - if !stale && prs[i].TurnResponse != nil { - for _, tag := range prs[i].TurnResponse.Analysis.Tags { - if tag == "stale" { - stale = true - break - } - } - } - - if !stale { + isStale := time.Since(prs[i].UpdatedAt) > staleAge || + (prs[i].TurnResponse != nil && slices.Contains(prs[i].TurnResponse.Analysis.Tags, "stale")) + if !isStale { filtered = append(filtered, prs[i]) } } @@ -1163,7 +1098,9 @@ func generatePRDisplay(prs []PR, username string, blockingOnly, includeStale boo } // Sort PRs by most recently updated - sortPRsByUpdateTime(prs) + sort.Slice(prs, func(i, j int) bool { + return prs[i].UpdatedAt.After(prs[j].UpdatedAt) + }) // Split into incoming and outgoing incoming, outgoing := categorizePRs(prs, username) @@ -1324,10 +1261,7 @@ func formatPR(pr *PR, username string) string { } // Reserve space: bullet(2) + space(1) + url + some padding(5) - availableForTitle := termWidth - 2 - 1 - urlLength - titlePadding - if availableForTitle < minTitleLength { - availableForTitle = minTitleLength // Minimum title length - } + availableForTitle := max(termWidth-2-1-urlLength-titlePadding, minTitleLength) // Title - truncated based on available space title := pr.Title @@ -1364,18 +1298,3 @@ func formatPR(pr *PR, username string) string { output.WriteString("\n") return output.String() } - -func orgFromURL(urlStr string) string { - // Extract org/owner from GitHub URL - parts := strings.Split(urlStr, "/") - if len(parts) >= minOrgURLParts && strings.Contains(urlStr, "github.com") { - return parts[3] // This is the org/owner - } - return "" -} - -func sortPRsByUpdateTime(prs []PR) { - sort.Slice(prs, func(i, j int) bool { - return prs[i].UpdatedAt.After(prs[j].UpdatedAt) - }) -} From da7e920619c409543d0cb0afa667d5fb8172c890 Mon Sep 17 00:00:00 2001 From: Thomas Stromberg Date: Sat, 17 Jan 2026 11:31:44 -0500 Subject: [PATCH 3/3] code cleanup --- main.go | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/main.go b/main.go index 9bd64ac..4a89a4f 100644 --- a/main.go +++ b/main.go @@ -559,9 +559,7 @@ func enrichPRsParallel(ctx context.Context, prs []PR, cfg *enrichConfig) { wg.Wait() } -func fetchPRDetails(ctx context.Context, pr *PR, token string, httpClient *http.Client, logger *log.Logger, debug bool) error { - // Extract repository info from PR URL - // URL format: https://github.com/owner/repo/pull/123 +func fetchPRDetails(ctx context.Context, pr *PR, cfg *enrichConfig) error { parts := strings.Split(pr.HTMLURL, "/") if len(parts) < minPRURLParts { return fmt.Errorf("invalid PR URL format: %s", pr.HTMLURL) @@ -569,20 +567,17 @@ func fetchPRDetails(ctx context.Context, pr *PR, token string, httpClient *http. owner := parts[3] repo := parts[repoPartIndex] - // Build API URL apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls/%d", owner, repo, pr.Number) - // Create request req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, http.NoBody) if err != nil { return err } - req.Header.Set("Authorization", "token "+token) + req.Header.Set("Authorization", "token "+cfg.token) req.Header.Set("Accept", "application/vnd.github.v3+json") - // Make request - resp, err := httpClient.Do(req) + resp, err := cfg.httpClient.Do(req) if err != nil { return err } @@ -592,19 +587,17 @@ func fetchPRDetails(ctx context.Context, pr *PR, token string, httpClient *http. return fmt.Errorf("GitHub API returned status %d", resp.StatusCode) } - // Parse response var prDetails PR if err := json.NewDecoder(resp.Body).Decode(&prDetails); err != nil { return err } - // Update PR with size information pr.Additions = prDetails.Additions pr.Deletions = prDetails.Deletions pr.ChangedFiles = prDetails.ChangedFiles - if debug { - logger.Printf("Fetched PR #%d size: +%d/-%d files:%d", pr.Number, pr.Additions, pr.Deletions, pr.ChangedFiles) + if cfg.debug { + cfg.logger.Printf("Fetched PR #%d size: +%d/-%d files:%d", pr.Number, pr.Additions, pr.Deletions, pr.ChangedFiles) } return nil @@ -619,7 +612,7 @@ func enrichPRData(ctx context.Context, pr *PR, cfg *enrichConfig) error { }() // Fetch individual PR data to get size information - if err := fetchPRDetails(ctx, pr, cfg.token, cfg.httpClient, cfg.logger, cfg.debug); err != nil { + if err := fetchPRDetails(ctx, pr, cfg); err != nil { cfg.logger.Printf("WARNING: Failed to fetch PR details for #%d: %v", pr.Number, err) // Continue without size info } @@ -631,9 +624,6 @@ func enrichPRData(ctx context.Context, pr *PR, cfg *enrichConfig) error { } return nil } - if cfg.debug { - cfg.logger.Printf("Calling enrichWithTurnData for PR #%d", pr.Number) - } return enrichWithTurnData(ctx, pr, cfg) } @@ -663,11 +653,11 @@ func enrichWithTurnData(ctx context.Context, pr *PR, cfg *enrichConfig) error { cfg.logger.Printf("INFO: Cache miss for PR #%d", pr.Number) } } else if cfg.debug { + msg := "Cache unavailable" if cfg.noCache { - cfg.logger.Printf("INFO: Cache disabled (--no-cache) for PR #%d", pr.Number) - } else { - cfg.logger.Printf("INFO: Cache unavailable for PR #%d", pr.Number) + msg = "Cache disabled (--no-cache)" } + cfg.logger.Printf("INFO: %s for PR #%d", msg, pr.Number) } return fetchAndCacheTurnData(ctx, pr, cacheKey, cfg)