Skip to content
Open
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ ranked := dependents.Rank(kept, 10, nil)

`DefaultScore` favors source references and tests after checkout analysis. Callers can pass another `ScoreFunc` to `Rank`, so security exposure and contract-test selection can use different policies.

`Analyze` accepts any `Checkout`. `CloneCheckout` uses a direct checkout and supports full history for Hyrum, while `CacheCheckout` copies from a persistent `git-pkgs/clone` cache for Scrutineer. A caller that supplies `Workdir` or sets `Keep` receives the checkout path on each analyzed candidate for follow-up work.
`Analyze` accepts any `Checkout`. `CloneCheckout` uses a direct checkout and supports full history for Hyrum, while `CacheCheckout` copies from a persistent `git-pkgs/clone` cache for Scrutineer. A caller that supplies `Workdir` or sets `Keep` receives the checkout path on each analyzed candidate for follow-up work. Set `DetectNativeExtensions` to record native-extension toolchains and their build commands, including Maturin, napi-rs, Neon, rb-sys, Rustler, and setuptools-rust.

After analysis, `FilterOptions.RequireTests` and `RequireImports` reproduce the contract-test eligibility used by downstream. Scrutineer can require upstream references without excluding repositories that have no conventional test files.

Expand Down
51 changes: 45 additions & 6 deletions analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"sync"

"github.com/git-pkgs/brief"
"github.com/git-pkgs/brief/detect"
"github.com/git-pkgs/brief/kb"
)

Expand All @@ -23,10 +25,11 @@ const (

// AnalyzeOptions controls checkout analysis.
type AnalyzeOptions struct {
Upstreams []string
Workdir string
Checkout Checkout
Keep bool
Upstreams []string
Workdir string
Checkout Checkout
Keep bool
DetectNativeExtensions bool
}

// AnalysisFailure records one candidate that could not be checked out or
Expand Down Expand Up @@ -85,6 +88,13 @@ func Analyze(ctx context.Context, candidates []Candidate, opts AnalyzeOptions) (
result.Failures = append(result.Failures, AnalysisFailure{Repository: candidate.Repository, Err: err})
continue
}
if opts.DetectNativeExtensions {
analysis.NativeExtensions, err = detectNativeExtensions(destination)
if err != nil {
result.Failures = append(result.Failures, AnalysisFailure{Repository: candidate.Repository, Err: err})
continue
}
}
candidate.Analysis = analysis
candidate.Analyzed = true
candidate.Commit = commit
Expand Down Expand Up @@ -150,9 +160,13 @@ func candidateDirectory(repository string) string {
return hex.EncodeToString(sum[:])
}

var loadKnowledge = sync.OnceValues(func() (*kb.KnowledgeBase, error) {
return kb.Load(brief.KnowledgeFS)
})

var testDirs = sync.OnceValue(func() map[string]bool {
dirs := map[string]bool{}
knowledge, err := kb.Load(brief.KnowledgeFS)
dirs := make(map[string]bool)
knowledge, err := loadKnowledge()
if err != nil {
return dirs
}
Expand All @@ -162,6 +176,31 @@ var testDirs = sync.OnceValue(func() map[string]bool {
return dirs
})

func detectNativeExtensions(root string) ([]NativeExtension, error) {
knowledge, err := loadKnowledge()
if err != nil {
return nil, err
}
report, err := detect.New(knowledge, root).Run()
if err != nil {
return nil, err
}

detections := report.Tools["native_extension"]
extensions := make([]NativeExtension, 0, len(detections))
for _, detection := range detections {
extension := NativeExtension{Name: detection.Name}
if detection.Command != nil {
extension.BuildCommand = detection.Command.Run
}
extensions = append(extensions, extension)
}
sort.Slice(extensions, func(i, j int) bool {
return extensions[i].Name < extensions[j].Name
})
return extensions, nil
}

func isTestFile(base string) bool {
stem, extension, ok := strings.Cut(base, ".")
if !ok {
Expand Down
52 changes: 50 additions & 2 deletions analyze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
Expand Down Expand Up @@ -71,7 +72,7 @@ func TestAnalyzeDirectorySkipsSymlinks(t *testing.T) {
if err != nil {
t.Fatalf("AnalyzeDirectory: %v", err)
}
if got != (Analysis{}) {
if !reflect.DeepEqual(got, Analysis{}) {
t.Errorf("analysis = %+v, want symlink excluded", got)
}
}
Expand Down Expand Up @@ -105,7 +106,7 @@ func TestAnalyzeKeepsFailuresAndPersistentDirectories(t *testing.T) {
t.Fatalf("result = %+v", result)
}
good := result.Candidates[0]
if !good.Analyzed || good.Commit != "abc123" || good.Analysis != (Analysis{TestFiles: 1, ImportFiles: 1}) {
if !good.Analyzed || good.Commit != "abc123" || !reflect.DeepEqual(good.Analysis, Analysis{TestFiles: 1, ImportFiles: 1}) {
t.Errorf("good candidate = %+v", good)
}
if good.Directory == "" {
Expand All @@ -125,6 +126,53 @@ func TestAnalyzeKeepsFailuresAndPersistentDirectories(t *testing.T) {
}
}

func TestAnalyzeDetectsNativeExtensions(t *testing.T) {
checkout := CheckoutFunc(func(_ context.Context, _ string, destination string) (string, error) {
writeTree(t, destination, map[string]string{
"Cargo.toml": `[package]
name = "native-package"
version = "0.1.0"

[dependencies]
rb-sys = "0.9"
`,
"Gemfile": `source "https://rubygems.org"
gem "rb_sys"
`,
"pyproject.toml": `[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"

[project]
name = "native-package"
version = "0.1.0"

[tool.maturin]
bindings = "pyo3"
`,
})
return "abc123", nil
})

result, err := Analyze(context.Background(), []Candidate{{Repository: "https://example.com/native"}}, AnalyzeOptions{
Checkout: checkout,
DetectNativeExtensions: true,
})
if err != nil {
t.Fatalf("Analyze: %v", err)
}
if len(result.Failures) != 0 {
t.Fatalf("failures = %+v", result.Failures)
}
want := []NativeExtension{
{Name: "Maturin", BuildCommand: "maturin develop"},
{Name: "rb-sys", BuildCommand: "bundle exec rake compile"},
}
if got := result.Candidates[0].Analysis.NativeExtensions; !reflect.DeepEqual(got, want) {
t.Errorf("native extensions = %+v, want %+v", got, want)
}
}

func TestAnalyzeStopsOnCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
Expand Down
13 changes: 10 additions & 3 deletions candidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,17 @@ type Relationship struct {
Dependent PackageRef
}

// Analysis contains checkout-derived ranking signals.
// NativeExtension describes a detected native-extension toolchain.
type NativeExtension struct {
Name string
BuildCommand string
}

// Analysis contains checkout-derived ranking signals and integrations.
type Analysis struct {
TestFiles int
ImportFiles int
TestFiles int
ImportFiles int
NativeExtensions []NativeExtension
}

// Candidate is one repository containing packages that depend on one or more
Expand Down
31 changes: 23 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,42 @@ module github.com/git-pkgs/dependents
go 1.26

require (
github.com/git-pkgs/brief v0.9.4
github.com/git-pkgs/clone v0.2.1
github.com/git-pkgs/brief v0.10.0
github.com/git-pkgs/clone v0.3.0
github.com/git-pkgs/enrichment v0.6.5
)

require (
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/bazelbuild/buildtools v0.0.0-20260716142318-04cf7de1434f // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/ecosyste-ms/ecosystems-go v0.4.0 // indirect
github.com/git-pkgs/magic v0.1.0 // indirect
github.com/git-pkgs/licensecheck v0.4.1 // indirect
github.com/git-pkgs/magic v0.2.0 // indirect
github.com/git-pkgs/manifests v0.8.0 // indirect
github.com/git-pkgs/packageurl-go v0.3.1 // indirect
github.com/git-pkgs/pom v0.1.5 // indirect
github.com/git-pkgs/purl v0.1.15 // indirect
github.com/git-pkgs/registries v0.6.4 // indirect
github.com/git-pkgs/spdx v0.3.0 // indirect
github.com/git-pkgs/vers v0.3.0 // indirect
github.com/git-pkgs/pom v0.1.6 // indirect
github.com/git-pkgs/purl v0.1.16 // indirect
github.com/git-pkgs/registries v0.7.0 // indirect
github.com/git-pkgs/spdx v0.3.1 // indirect
github.com/git-pkgs/vers v0.3.1 // indirect
github.com/git-pkgs/vulns v0.2.1 // indirect
github.com/github/go-spdx/v2 v2.7.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.1 // indirect
github.com/go-git/go-git/v5 v5.19.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/oapi-codegen/nullable v1.2.0 // indirect
github.com/oapi-codegen/runtime v1.6.0 // indirect
github.com/package-url/packageurl-go v0.1.6 // indirect
github.com/pandatix/go-cvss v0.6.2 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.47.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading