这是indexloc提供的服务,不要输入任何密码
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
41 changes: 24 additions & 17 deletions cmd/whatsun/cmd_analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

func analyzeCmd() *cobra.Command {
var ignore []string
var plain bool
cmd := &cobra.Command{
Use: "analyze [path]",
Short: "Analyze a code repository and show results",
Expand All @@ -30,16 +31,18 @@ func analyzeCmd() *cobra.Command {
if len(args) > 0 {
path = args[0]
}
return runAnalyze(cmd.Context(), path, ignore, cmd.OutOrStdout(), cmd.ErrOrStderr())
return runAnalyze(cmd.Context(), path, ignore, plain, cmd.OutOrStdout(), cmd.ErrOrStderr())
},
}
cmd.Flags().StringSliceVar(&ignore, "ignore", []string{},
"Paths (or patterns) to ignore, adding to defaults.")
cmd.Flags().BoolVar(&plain, "plain", false,
"Output plain tab-separated values with header row.")

return cmd
}

func runAnalyze(ctx context.Context, path string, ignore []string, stdout, stderr io.Writer) error {
func runAnalyze(ctx context.Context, path string, ignore []string, plain bool, stdout, stderr io.Writer) error {
fsys, disableGitIgnore, err := setupFileSystem(ctx, path, stderr)
if err != nil {
return err
Expand Down Expand Up @@ -76,26 +79,30 @@ func runAnalyze(ctx context.Context, path string, ignore []string, stdout, stder
return nil
}

tbl := table.NewWriter()
tbl.AppendHeader(table.Row{"Path", "Ruleset", "Result", "Groups", "With"})
if plain {
outputAnalyzePlain(reports, stdout)
} else {
tbl := table.NewWriter()
tbl.AppendHeader(table.Row{"Path", "Ruleset", "Result", "Groups", "With"})

for _, report := range reports {
if report.Maybe {
continue
}
var with string
if len(report.With) > 0 {
for k, v := range report.With {
if v.Error == "" && !isEmpty(v.Value) {
with += fmt.Sprintf("%s: %s\n", k, v.Value)
for _, report := range reports {
if report.Maybe {
continue
}
var with string
if len(report.With) > 0 {
for k, v := range report.With {
if v.Error == "" && !isEmpty(v.Value) {
with += fmt.Sprintf("%s: %s\n", k, v.Value)
}
}
with = strings.TrimSpace(with)
}
with = strings.TrimSpace(with)
tbl.AppendRow(table.Row{report.Path, report.Ruleset, report.Result, strings.Join(report.Groups, ", "), with})
}
tbl.AppendRow(table.Row{report.Path, report.Ruleset, report.Result, strings.Join(report.Groups, ", "), with})
}

fmt.Fprintln(stdout, tbl.Render())
fmt.Fprintln(stdout, tbl.Render())
}

return nil
}
Expand Down
207 changes: 207 additions & 0 deletions cmd/whatsun/cmd_deps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package main

import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"slices"
"strings"

"github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"

"github.com/upsun/whatsun/internal/fsgitignore"
"github.com/upsun/whatsun/pkg/dep"
)

func depsCmd() *cobra.Command {
var ignore []string
var includeIndirect bool
var includeDev bool
var plain bool
cmd := &cobra.Command{
Use: "deps [path]",
Short: "List dependencies found in the repository",
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveFilterDirs
},
SilenceErrors: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
path := "."
if len(args) > 0 {
path = args[0]
}
return runDeps(cmd.Context(), path, ignore, includeIndirect, includeDev, plain, cmd.OutOrStdout(), cmd.ErrOrStderr())
},
}
cmd.Flags().StringSliceVar(&ignore, "ignore", []string{},
"Paths (or patterns) to ignore, adding to defaults.")
cmd.Flags().BoolVar(&includeIndirect, "include-indirect", false,
"Include indirect/transitive dependencies in addition to direct dependencies.")
cmd.Flags().BoolVar(&includeDev, "include-dev", false,
"Include development-only dependencies.")
cmd.Flags().BoolVar(&plain, "plain", false,
"Output plain tab-separated values with header row.")

return cmd
}

func runDeps(
ctx context.Context,
path string,
ignore []string,
includeIndirect, includeDev, plain bool,
stdout, stderr io.Writer,
) error {
fsys, disableGitIgnore, err := setupFileSystem(ctx, path, stderr)
if err != nil {
return err
}

dependencies, err := collectAllDependencies(ctx, fsys, ignore, disableGitIgnore)
if err != nil {
return fmt.Errorf("failed to collect dependencies: %w", err)
}

// Filter dependencies based on flags
var filteredDeps []dependencyInfo
for _, depInfo := range dependencies {
// Skip indirect dependencies if not requested
if !includeIndirect && !depInfo.Dependency.IsDirect {
continue
}
// Skip dev dependencies unless --include-dev is specified
if !includeDev && depInfo.Dependency.IsDevOnly {
continue
}
filteredDeps = append(filteredDeps, depInfo)
}

if len(filteredDeps) == 0 {
if !includeIndirect {
fmt.Fprintln(stderr, "No direct dependencies found.")
} else {
fmt.Fprintln(stderr, "No dependencies found.")
}
return nil
}

if plain {
outputDepsPlain(filteredDeps, stdout)
} else {
tbl := table.NewWriter()
tbl.AppendHeader(table.Row{"Path", "Tool", "Name", "Constraint", "Version"})

for _, depInfo := range filteredDeps {
tbl.AppendRow(table.Row{
depInfo.Path,
depInfo.Dependency.ToolName,
depInfo.Dependency.Name,
depInfo.Dependency.Constraint,
depInfo.Dependency.Version,
})
}

fmt.Fprintln(stdout, tbl.Render())
}
return nil
}

type dependencyInfo struct {
Path string
Manager string
Dependency dep.Dependency
}

func collectAllDependencies(
ctx context.Context,
fsys fs.FS,
ignore []string,
disableGitIgnore bool,
) ([]dependencyInfo, error) {
var allDeps []dependencyInfo

var ignorePatterns = fsgitignore.GetDefaultIgnorePatterns()
if len(ignore) > 0 {
ignorePatterns = append(ignorePatterns, fsgitignore.ParsePatterns(ignore, fsgitignore.Split("."))...)
}

err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}

if err != nil {
return err
}

if !d.IsDir() {
return nil
}

// Hard-limit the directory depth to 16.
if strings.Count(path, string(os.PathSeparator)) >= 16 {
return filepath.SkipDir
}

// Skip .git and node_modules
if d.Name() == ".git" || d.Name() == "node_modules" {
return fs.SkipDir
}

// Apply gitignore patterns
if gitignore.NewMatcher(ignorePatterns).Match(fsgitignore.Split(path), true) {
return fs.SkipDir
}

// Parse additional .gitignore files if not disabled
if !disableGitIgnore {
patterns, err := fsgitignore.ParseIgnoreFiles(fsys, path)
if err != nil {
return err
}
ignorePatterns = append(ignorePatterns, patterns...)
}

// Try each manager type for this directory
for _, managerType := range dep.AllManagerTypes {
manager, err := dep.GetManager(managerType, fsys, path)
if err != nil {
return err
}

if err := manager.Init(); err != nil {
return err
}

// Get all dependencies by using a wildcard pattern
deps := manager.Find("*")
slices.SortStableFunc(deps, func(a, b dep.Dependency) int {
return strings.Compare(a.Name, b.Name)
})
for _, dependency := range deps {
allDeps = append(allDeps, dependencyInfo{
Path: path,
Manager: managerType,
Dependency: dependency,
})
}
}

return nil
})

if err != nil {
return nil, err
}

return allDeps, nil
}
2 changes: 1 addition & 1 deletion cmd/whatsun/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func main() {
Short: "Analyze a code repository",
}

rootCmd.AddCommand(analyzeCmd(), digestCmd(), treeCmd())
rootCmd.AddCommand(analyzeCmd(), digestCmd(), treeCmd(), depsCmd())

if err := rootCmd.Execute(); err != nil {
fmt.Println(color.RedString(err.Error()))
Expand Down
50 changes: 50 additions & 0 deletions cmd/whatsun/plain_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"fmt"
"io"
"strings"

"github.com/upsun/whatsun/pkg/rules"
)

// outputDepsPlain outputs dependencies in plain tab-separated format
func outputDepsPlain(deps []dependencyInfo, stdout io.Writer) {
fmt.Fprintln(stdout, "Path\tTool\tName\tConstraint\tVersion")
for _, depInfo := range deps {
fmt.Fprintf(stdout, "%s\t%s\t%s\t%s\t%s\n",
depInfo.Path,
depInfo.Dependency.ToolName,
depInfo.Dependency.Name,
depInfo.Dependency.Constraint,
depInfo.Dependency.Version,
)
}
}

// outputAnalyzePlain outputs analysis reports in plain tab-separated format
func outputAnalyzePlain(reports []rules.Report, stdout io.Writer) {
fmt.Fprintln(stdout, "Path\tRuleset\tResult\tGroups\tWith")
for _, report := range reports {
if report.Maybe {
continue
}
var with string
if len(report.With) > 0 {
var parts []string
for k, v := range report.With {
if v.Error == "" && !isEmpty(v.Value) {
parts = append(parts, fmt.Sprintf("%s: %s", k, v.Value))
}
}
with = strings.Join(parts, "; ")
}
fmt.Fprintf(stdout, "%s\t%s\t%s\t%s\t%s\n",
report.Path,
report.Ruleset,
report.Result,
strings.Join(report.Groups, ", "),
with,
)
}
}
3 changes: 3 additions & 0 deletions pkg/dep/dep.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ type Dependency struct {
Name string // The standard package name, which may include the vendor name.
Constraint string // The version constraint.
Version string // The resolved version (e.g. from a lock file).
IsDirect bool // True if explicitly specified in manifest, false if transitive/from lock file only.
IsDevOnly bool // True if this is a development-only dependency.
ToolName string // The external name of the tool that manages this dependency (e.g. "uv", "poetry", "composer").
}

type Manager interface {
Expand Down
Loading