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
13 changes: 4 additions & 9 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ linters:
struct-method: false

gocognit:
min-complexity: 65
min-complexity: 55

gocritic:
enable-all: true
Expand All @@ -143,11 +143,6 @@ linters:
govet:
enable-all: true

maintidx:
# Maintainability Index threshold (default: 20)
# ExtrapolateFromSamples is a straightforward calculation with clear linear logic
under: 16

godot:
scope: toplevel

Expand Down Expand Up @@ -182,7 +177,7 @@ linters:
- name: cyclomatic
disabled: true # prefer maintidx
- name: function-length
arguments: [150, 300]
arguments: [150, 225]
- name: line-length-limit
arguments: [150]
- name: nested-structs
Expand All @@ -191,8 +186,8 @@ linters:
arguments: [10]
- name: flag-parameter # fixes are difficult
disabled: true
- name: use-waitgroup-go
disabled: true # wg.Add/Done pattern is idiomatic Go
- name: bare-return
disabled: true

rowserrcheck:
# database/sql is always checked.
Expand Down
49 changes: 48 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,28 @@ endif
LINTERS :=
FIXERS :=

SHELLCHECK_VERSION ?= v0.11.0
SHELLCHECK_BIN := $(LINT_ROOT)/out/linters/shellcheck-$(SHELLCHECK_VERSION)-$(LINT_ARCH)
$(SHELLCHECK_BIN):
mkdir -p $(LINT_ROOT)/out/linters
curl -sSfL -o [email protected] https://github.com/koalaman/shellcheck/releases/download/$(SHELLCHECK_VERSION)/shellcheck-$(SHELLCHECK_VERSION).$(LINT_OS_LOWER).$(LINT_ARCH).tar.xz \
|| echo "Unable to fetch shellcheck for $(LINT_OS)/$(LINT_ARCH): falling back to locally install"
test -f [email protected] \
&& tar -C $(LINT_ROOT)/out/linters -xJf [email protected] \
&& mv $(LINT_ROOT)/out/linters/shellcheck-$(SHELLCHECK_VERSION)/shellcheck $@ \
|| printf "#!/usr/bin/env shellcheck\n" > $@
chmod u+x $@

LINTERS += shellcheck-lint
shellcheck-lint: $(SHELLCHECK_BIN)
$(SHELLCHECK_BIN) $(shell find . -name "*.sh")

FIXERS += shellcheck-fix
shellcheck-fix: $(SHELLCHECK_BIN)
$(SHELLCHECK_BIN) $(shell find . -name "*.sh") -f diff | { read -t 1 line || exit 0; { echo "$$line" && cat; } | git apply -p2; }

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
Expand Down Expand Up @@ -61,6 +81,32 @@ LINTERS += yamllint-lint
yamllint-lint: $(YAMLLINT_BIN)
PYTHONPATH=$(YAMLLINT_ROOT)/dist $(YAMLLINT_ROOT)/dist/bin/yamllint .

BIOME_VERSION ?= 2.3.8
BIOME_BIN := $(LINT_ROOT)/out/linters/biome-$(BIOME_VERSION)-$(LINT_ARCH)
BIOME_CONFIG := $(LINT_ROOT)/biome.json

# Map architecture names for Biome downloads
BIOME_ARCH := $(LINT_ARCH)
ifeq ($(LINT_ARCH),x86_64)
BIOME_ARCH := x64
endif

$(BIOME_BIN):
mkdir -p $(LINT_ROOT)/out/linters
rm -rf $(LINT_ROOT)/out/linters/biome-*
curl -sSfL -o $@ https://github.com/biomejs/biome/releases/download/%40biomejs%2Fbiome%40$(BIOME_VERSION)/biome-$(LINT_OS_LOWER)-$(BIOME_ARCH) \
|| echo "Unable to fetch biome for $(LINT_OS_LOWER)/$(BIOME_ARCH), falling back to local install"
test -f $@ || printf "#!/usr/bin/env biome\n" > $@
chmod u+x $@

LINTERS += biome-lint
biome-lint: $(BIOME_BIN)
$(BIOME_BIN) check --config-path=$(BIOME_CONFIG) .

FIXERS += biome-fix
biome-fix: $(BIOME_BIN)
$(BIOME_BIN) check --write --config-path=$(BIOME_CONFIG) .

.PHONY: _lint $(LINTERS)
_lint:
@exit_code=0; \
Expand All @@ -79,6 +125,7 @@ fix:

# END: lint-install .


.PHONY: deploy
deploy:
./hacks/deploy.sh cmd/server/
10 changes: 2 additions & 8 deletions cmd/prcost/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,19 +590,13 @@ func printEfficiency(breakdown *cost.Breakdown) {

fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
headerText := fmt.Sprintf("DEVELOPMENT EFFICIENCY: %s (%.1f%%) - %s", grade, efficiencyPct, message)
padding := 60 - len(headerText)
if padding < 0 {
padding = 0
}
padding := max(60-len(headerText), 0)
fmt.Printf(" │ %s%*s│\n", headerText, padding, "")
fmt.Println(" └─────────────────────────────────────────────────────────────┘")

fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
velocityHeader := fmt.Sprintf("MERGE VELOCITY: %s (%s) - %s", velocityGrade, formatTimeUnit(breakdown.PRDuration), velocityMessage)
velPadding := 60 - len(velocityHeader)
if velPadding < 0 {
velPadding = 0
}
velPadding := max(60-len(velocityHeader), 0)
fmt.Printf(" │ %s%*s│\n", velocityHeader, velPadding, "")
fmt.Println(" └─────────────────────────────────────────────────────────────┘")

Expand Down
86 changes: 46 additions & 40 deletions cmd/prcost/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import (
// Uses library functions from pkg/github and pkg/cost for fetching, sampling,
// and extrapolation - all functionality is available to external clients.
func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days int, cfg cost.Config, token, dataSource string) error {
// Create GitHub client without caching (for CLI)
client := github.NewClientWithoutCache()

// Calculate since date
since := time.Now().AddDate(0, 0, -days)

// Fetch all PRs modified since the date using library function
prs, err := github.FetchPRsFromRepo(ctx, owner, repo, since, token, nil)
prs, err := client.FetchPRsFromRepo(ctx, owner, repo, since, token, nil)
if err != nil {
return fmt.Errorf("failed to fetch PRs: %w", err)
}
Expand Down Expand Up @@ -59,14 +62,14 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
}

// Convert samples to PRSummaryInfo format
var summaries []cost.PRSummaryInfo
for _, pr := range samples {
summaries = append(summaries, cost.PRSummaryInfo{
Owner: pr.Owner,
Repo: pr.Repo,
Number: pr.Number,
UpdatedAt: pr.UpdatedAt,
})
summaries := make([]cost.PRSummaryInfo, len(samples))
for i := range samples {
summaries[i] = cost.PRSummaryInfo{
Owner: samples[i].Owner,
Repo: samples[i].Repo,
Number: samples[i].Number,
UpdatedAt: samples[i].UpdatedAt,
}
}

// Create fetcher
Expand All @@ -93,25 +96,25 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
totalAuthors := github.CountUniqueAuthors(prs)

// Query for actual count of open PRs (not extrapolated from samples)
openPRCount, err := github.CountOpenPRsInRepo(ctx, owner, repo, token)
openPRCount, err := client.CountOpenPRsInRepo(ctx, owner, repo, token)
if err != nil {
slog.Warn("Failed to count open PRs, using 0", "error", err)
openPRCount = 0
}

// Convert PRSummary to PRSummaryInfo for extrapolation
prSummaryInfos := make([]cost.PRSummaryInfo, len(prs))
for i, pr := range prs {
for i := range prs {
prSummaryInfos[i] = cost.PRSummaryInfo{
Owner: pr.Owner,
Repo: pr.Repo,
Author: pr.Author,
AuthorType: pr.AuthorType,
CreatedAt: pr.CreatedAt,
UpdatedAt: pr.UpdatedAt,
ClosedAt: pr.ClosedAt,
Merged: pr.Merged,
State: pr.State,
Owner: prs[i].Owner,
Repo: prs[i].Repo,
Author: prs[i].Author,
AuthorType: prs[i].AuthorType,
CreatedAt: prs[i].CreatedAt,
UpdatedAt: prs[i].UpdatedAt,
ClosedAt: prs[i].ClosedAt,
Merged: prs[i].Merged,
State: prs[i].State,
}
}

Expand All @@ -128,13 +131,16 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
// Uses library functions from pkg/github and pkg/cost for fetching, sampling,
// and extrapolation - all functionality is available to external clients.
func analyzeOrganization(ctx context.Context, org string, sampleSize, days int, cfg cost.Config, token, dataSource string) error {
// Create GitHub client without caching (for CLI)
client := github.NewClientWithoutCache()

slog.Info("Fetching PR list from organization")

// Calculate since date
since := time.Now().AddDate(0, 0, -days)

// Fetch all PRs across the org modified since the date using library function
prs, err := github.FetchPRsFromOrg(ctx, org, since, token, nil)
prs, err := client.FetchPRsFromOrg(ctx, org, since, token, nil)
if err != nil {
return fmt.Errorf("failed to fetch PRs: %w", err)
}
Expand Down Expand Up @@ -174,14 +180,14 @@ func analyzeOrganization(ctx context.Context, org string, sampleSize, days int,
}

// Convert samples to PRSummaryInfo format
var summaries []cost.PRSummaryInfo
for _, pr := range samples {
summaries = append(summaries, cost.PRSummaryInfo{
Owner: pr.Owner,
Repo: pr.Repo,
Number: pr.Number,
UpdatedAt: pr.UpdatedAt,
})
summaries := make([]cost.PRSummaryInfo, len(samples))
for i := range samples {
summaries[i] = cost.PRSummaryInfo{
Owner: samples[i].Owner,
Repo: samples[i].Repo,
Number: samples[i].Number,
UpdatedAt: samples[i].UpdatedAt,
}
}

// Create fetcher
Expand All @@ -208,7 +214,7 @@ func analyzeOrganization(ctx context.Context, org string, sampleSize, days int,
totalAuthors := github.CountUniqueAuthors(prs)

// Count open PRs across the entire organization with a single query
totalOpenPRs, err := github.CountOpenPRsInOrg(ctx, org, token)
totalOpenPRs, err := client.CountOpenPRsInOrg(ctx, org, token)
if err != nil {
slog.Warn("Failed to count open PRs in organization, using 0", "error", err)
totalOpenPRs = 0
Expand All @@ -217,17 +223,17 @@ func analyzeOrganization(ctx context.Context, org string, sampleSize, days int,

// Convert PRSummary to PRSummaryInfo for extrapolation
prSummaryInfos := make([]cost.PRSummaryInfo, len(prs))
for i, pr := range prs {
for i := range prs {
prSummaryInfos[i] = cost.PRSummaryInfo{
Owner: pr.Owner,
Repo: pr.Repo,
Author: pr.Author,
AuthorType: pr.AuthorType,
CreatedAt: pr.CreatedAt,
UpdatedAt: pr.UpdatedAt,
ClosedAt: pr.ClosedAt,
Merged: pr.Merged,
State: pr.State,
Owner: prs[i].Owner,
Repo: prs[i].Repo,
Author: prs[i].Author,
AuthorType: prs[i].AuthorType,
CreatedAt: prs[i].CreatedAt,
UpdatedAt: prs[i].UpdatedAt,
ClosedAt: prs[i].ClosedAt,
Merged: prs[i].Merged,
State: prs[i].State,
}
}

Expand Down
4 changes: 4 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ func main() {

// Create server
prcostServer := server.New()
if prcostServer == nil {
logger.ErrorContext(ctx, "failed to initialize server (check cache configuration)")
os.Exit(1)
}
prcostServer.SetCommit(GitCommit)
prcostServer.SetCORSConfig(*corsOrigins, *allowAllCors)
prcostServer.SetRateLimit(*rateLimit, *rateBurst)
Expand Down
19 changes: 14 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
module github.com/codeGROOVE-dev/prcost

go 1.25.3
go 1.25.4

require (
github.com/codeGROOVE-dev/ds9 v0.6.0
github.com/codeGROOVE-dev/fido v1.10.0
github.com/codeGROOVE-dev/fido/pkg/store/cloudrun v1.10.0
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22
github.com/codeGROOVE-dev/prx v0.0.0-20251030022101-ff906928a1e4
github.com/codeGROOVE-dev/turnclient v0.0.0-20251030022425-bc3b14acf75e
github.com/codeGROOVE-dev/prx v0.0.0-20251109164430-90488144076d
github.com/codeGROOVE-dev/turnclient v0.0.0-20251107215141-ee43672b3dc7
golang.org/x/time v0.14.0
)

require github.com/codeGROOVE-dev/retry v1.3.0 // indirect
require (
github.com/codeGROOVE-dev/ds9 v0.8.0 // indirect
github.com/codeGROOVE-dev/fido/pkg/store/compress v1.10.0 // indirect
github.com/codeGROOVE-dev/fido/pkg/store/datastore v1.10.0 // indirect
github.com/codeGROOVE-dev/fido/pkg/store/localfs v1.10.0 // indirect
github.com/codeGROOVE-dev/retry v1.3.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
)
28 changes: 22 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
github.com/codeGROOVE-dev/ds9 v0.6.0 h1:JG7vBH17UAKaVoeQilrIvA1I0fg3iNbdUMBSDS7ixgI=
github.com/codeGROOVE-dev/ds9 v0.6.0/go.mod h1:0UDipxF1DADfqM5GtjefgB2u+EXdDgOKmxVvrSGLHoM=
github.com/codeGROOVE-dev/ds9 v0.8.0 h1:A23VvL1YzUBZyXNYmF5u0R6nPcxQitPeLo8FFk6OiUs=
github.com/codeGROOVE-dev/ds9 v0.8.0/go.mod h1:0UDipxF1DADfqM5GtjefgB2u+EXdDgOKmxVvrSGLHoM=
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/cloudrun v1.10.0 h1:0Wvs3JE+TI8GsEkh0jg0SglyFyIkBewPSl0PTUSVqEo=
github.com/codeGROOVE-dev/fido/pkg/store/cloudrun v1.10.0/go.mod h1:MaxO6QGv89FrZB1D+stiZjRcbaMUfiw7yYGkaqOoJ2k=
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/datastore v1.10.0 h1:vCsLeESGQvW7F8pJJimZhRjzWmrQg1WZgT22om9fT/Q=
github.com/codeGROOVE-dev/fido/pkg/store/datastore v1.10.0/go.mod h1:LtpO9TUi92D7uLBXJu+kLWVpRmEtVRAWVB2EdzNU0JQ=
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/gsm v0.0.0-20251019065141-833fe2363d22 h1:gtN3rOc6YspO646BkcOxBhPjEqKUz+jl175jIqglfDg=
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22/go.mod h1:KV+w19ubP32PxZPE1hOtlCpTaNpF0Bpb32w5djO8UTg=
github.com/codeGROOVE-dev/prx v0.0.0-20251030022101-ff906928a1e4 h1:DSuoUwP3oyR4cHrX0cUh9c7CtYjXNIcyCmqpIwHilIU=
github.com/codeGROOVE-dev/prx v0.0.0-20251030022101-ff906928a1e4/go.mod h1:FEy3gz9IYDXWnKWkoDSL+pWu6rujxbBSrF4w5A8QSK0=
github.com/codeGROOVE-dev/prx v0.0.0-20251109164430-90488144076d h1:KKt93PVYR9Uga8uLPq0HoNlXVW3BTPHGBBxEb5YBxf4=
github.com/codeGROOVE-dev/prx v0.0.0-20251109164430-90488144076d/go.mod h1:FEy3gz9IYDXWnKWkoDSL+pWu6rujxbBSrF4w5A8QSK0=
github.com/codeGROOVE-dev/retry v1.3.0 h1:/+ipAWRJLL6y1R1vprYo0FSjSBvH6fE5j9LKXjpD54g=
github.com/codeGROOVE-dev/retry v1.3.0/go.mod h1:8OgefgV1XP7lzX2PdKlCXILsYKuz6b4ZpHa/20iLi8E=
github.com/codeGROOVE-dev/turnclient v0.0.0-20251030022425-bc3b14acf75e h1:WXHdC8o5KmP5CwkQRiGVywYzsj93fjkRPq7clhfZPq0=
github.com/codeGROOVE-dev/turnclient v0.0.0-20251030022425-bc3b14acf75e/go.mod h1:dVS3MlJDgL6WkfurJAyS7I9Fe1yxxoxxarjVifY5bIo=
github.com/codeGROOVE-dev/turnclient v0.0.0-20251107215141-ee43672b3dc7 h1:183q0bj2y/9hh/K0HZvDXI6sG7liYSRcQVgFx0GY+UA=
github.com/codeGROOVE-dev/turnclient v0.0.0-20251107215141-ee43672b3dc7/go.mod h1:dVS3MlJDgL6WkfurJAyS7I9Fe1yxxoxxarjVifY5bIo=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
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=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
7 changes: 2 additions & 5 deletions hacks/debug-sessions/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func main() {
events := make([]cost.ParticipantEvent, len(authorEvents))
copy(events, authorEvents)
// Sort manually
for i := 0; i < len(events); i++ {
for i := range events {
for j := i + 1; j < len(events); j++ {
if events[j].Timestamp.Before(events[i].Timestamp) {
events[i], events[j] = events[j], events[i]
Expand Down Expand Up @@ -105,10 +105,7 @@ func main() {
// Gaps between events
for j := start; j < end; j++ {
gap := events[j+1].Timestamp.Sub(events[j].Timestamp)
counted := gap
if gap > eventDur {
counted = eventDur
}
counted := min(gap, eventDur)
totalGitHub += counted
fmt.Printf(" Gap %d->%d: %v (actual: %v)\n", j-start, j-start+1, counted, gap)
}
Expand Down
2 changes: 1 addition & 1 deletion hacks/test-sessions/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func main() {
copy(sorted, authorEvents)

// Simple sort
for i := 0; i < len(sorted); i++ {
for i := range sorted {
for j := i + 1; j < len(sorted); j++ {
if sorted[j].Timestamp.Before(sorted[i].Timestamp) {
sorted[i], sorted[j] = sorted[j], sorted[i]
Expand Down
Loading
Loading