From dd1dba383f0ffd2dc7ad6e30454e2673b7b21066 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Fri, 9 Jan 2026 19:31:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E7=BA=A7=E4=BE=9D=E8=B5=96=E5=88=86=E6=9E=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/gf/internal/cmd/cmddep/cmddep.go | 42 +- cmd/gf/internal/cmd/cmddep/cmddep_analyzer.go | 515 +++++++++++++++--- .../cmd/cmddep/cmddep_external_test.go | 42 +- cmd/gf/internal/cmd/cmddep/cmddep_output.go | 1 - cmd/gf/internal/cmd/cmddep/cmddep_server.go | 414 ++++++++++++-- .../internal/cmd/cmddep/cmddep_z_unit_test.go | 141 +++++ cmd/gf/internal/cmd/cmddep/static/app.js | 53 +- cmd/gf/internal/cmd/cmddep/static/i18n.js | 12 +- cmd/gf/internal/cmd/cmddep/static/index.html | 8 +- 9 files changed, 1078 insertions(+), 150 deletions(-) diff --git a/cmd/gf/internal/cmd/cmddep/cmddep.go b/cmd/gf/internal/cmd/cmddep/cmddep.go index 6d7739b3e..e74ff9ce7 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep.go @@ -39,13 +39,13 @@ gf dep -r gf dep -i=false gf dep -e gf dep -e -i=false -gf dep -m -gf dep -m -e +gf dep -M +gf dep -M -D gf dep -s gf dep -s -p 8080 gf dep ./internal/... -f tree -d 2 gf dep --external --group -f mermaid -gf dep --main --external -f json +gf dep --module --direct -f json ` ) @@ -62,12 +62,13 @@ type Input struct { Package string `name:"PACKAGE" arg:"true" brief:"package path to analyze, default is ./..." d:"./..."` Format string `name:"format" short:"f" brief:"output format: tree/list/mermaid/dot/json" d:"tree"` Depth int `name:"depth" short:"d" brief:"dependency depth limit, 0 means unlimited" d:"3"` - Group bool `name:"group" short:"g" brief:"group by top-level directory" d:"false"` - Internal bool `name:"internal" short:"i" brief:"show only internal packages" d:"true"` - External bool `name:"external" short:"e" brief:"show external packages" d:"false"` - MainOnly bool `name:"main" short:"m" brief:"analyze only main module packages (exclude submodules)" d:"false"` - NoStd bool `name:"nostd" short:"n" brief:"exclude standard library" d:"true"` - Reverse bool `name:"reverse" short:"r" brief:"show reverse dependencies" d:"false"` + Group bool `name:"group" short:"g" brief:"group by top-level directory" d:"false" orphan:"true"` + Internal bool `name:"internal" short:"i" brief:"show only internal packages" d:"true" orphan:"true"` + External bool `name:"external" short:"e" brief:"show external packages" d:"false" orphan:"true"` + Module bool `name:"module" short:"M" brief:"show module-level dependencies (from go.mod)" d:"false" orphan:"true"` + Direct bool `name:"direct" short:"D" brief:"show only direct dependencies (requires --module)" d:"false" orphan:"true"` + NoStd bool `name:"nostd" short:"n" brief:"exclude standard library" d:"true" orphan:"true"` + Reverse bool `name:"reverse" short:"r" brief:"show reverse dependencies" d:"false" orphan:"true"` Serve bool `name:"serve" short:"s" brief:"start HTTP server to view dependencies" d:"false" orphan:"true"` Port int `name:"port" short:"p" brief:"HTTP server port" d:"8888"` } @@ -82,7 +83,28 @@ func (c cDep) Index(ctx context.Context, in Input) (out *Output, err error) { // Detect module prefix from go.mod analyzer.modulePrefix = analyzer.detectModulePrefix() - // Get package information + // Module-level analysis mode (uses go mod graph) + if in.Module { + if in.Serve { + // Load module graph for server mode + if err := analyzer.loadModuleGraph(ctx); err != nil { + mlog.Print("Warning: Failed to load module graph: " + err.Error()) + } + return nil, analyzer.startServer(in) + } + + // Load module graph + if err := analyzer.loadModuleGraph(ctx); err != nil { + return nil, err + } + + // Generate module-level output + output := analyzer.generateModuleOutput(in) + mlog.Print(output) + return + } + + // Package-level analysis mode (original behavior) loadErr := analyzer.loadPackages(ctx, in.Package) // Start HTTP server if requested diff --git a/cmd/gf/internal/cmd/cmddep/cmddep_analyzer.go b/cmd/gf/internal/cmd/cmddep/cmddep_analyzer.go index dc568161b..beb0d7e10 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep_analyzer.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep_analyzer.go @@ -49,31 +49,29 @@ const ( // It consolidates package information from go list output with additional // metadata for filtering and traversal. type PackageInfo struct { - ImportPath string // Full import path (e.g., github.com/gogf/gf/v2/os/gfile) - ModulePath string // Module path (e.g., github.com/gogf/gf/v2) - Kind PackageKind // Package classification (Internal/External/StdLib) - Tier int // Package tier: 0=module root, 1=top-level, 2+=nested - Imports []string // Direct imports of this package - IsStdLib bool // Standard library marker (from go list) - IsModuleRoot bool // Is this the root package of its module - IsMainModule bool // Is this package from the main module (not a submodule) + ImportPath string // Full import path (e.g., github.com/gogf/gf/v2/os/gfile) + ModulePath string // Module path (e.g., github.com/gogf/gf/v2) + Kind PackageKind // Package classification (Internal/External/StdLib) + Tier int // Package tier: 0=module root, 1=top-level, 2+=nested + Imports []string // Direct imports of this package + IsStdLib bool // Standard library marker (from go list) + IsModuleRoot bool // Is this the root package of its module } // FilterOptions represents filtering criteria for dependency analysis. // It provides a clear, normalized representation of user filtering preferences. // Usage: -// opts := &FilterOptions{ -// IncludeInternal: true, -// IncludeExternal: false, -// IncludeStdLib: false, -// MainModuleOnly: false, -// Depth: 3, -// } +// +// opts := &FilterOptions{ +// IncludeInternal: true, +// IncludeExternal: false, +// IncludeStdLib: false, +// Depth: 3, +// } type FilterOptions struct { IncludeInternal bool // Include internal packages from main module IncludeExternal bool // Include external dependencies IncludeStdLib bool // Include standard library packages - MainModuleOnly bool // Only analyze packages from main module (exclude submodules) Depth int // Maximum traversal depth (0 = unlimited) } @@ -106,15 +104,30 @@ type analyzer struct { visited map[string]bool edges map[string]bool store *PackageStore // New unified package store + // Module-level dependency data (from go mod graph) + modules map[string]*ModuleInfo // All modules indexed by path + moduleGraph map[string][]string // Module dependency graph: module -> dependencies + directModules map[string]bool // Direct dependencies (from go.mod require) +} + +// ModuleInfo represents a Go module dependency. +type ModuleInfo struct { + Path string // Module path (e.g., github.com/gin-gonic/gin) + Version string // Module version (e.g., v1.9.0) + IsDirect bool // Is this a direct dependency + Deps []string // Dependencies of this module } // newAnalyzer creates a new dependency analyzer. func newAnalyzer() *analyzer { return &analyzer{ - packages: make(map[string]*goPackage), - visited: make(map[string]bool), - edges: make(map[string]bool), - store: &PackageStore{}, + packages: make(map[string]*goPackage), + visited: make(map[string]bool), + edges: make(map[string]bool), + store: &PackageStore{}, + modules: make(map[string]*ModuleInfo), + moduleGraph: make(map[string][]string), + directModules: make(map[string]bool), } } @@ -220,7 +233,423 @@ func (a *analyzer) loadPackages(ctx context.Context, pkgPath string) error { return nil } +// loadModuleGraph loads module-level dependencies using go mod graph. +// This provides module-level dependency information (not package-level). +func (a *analyzer) loadModuleGraph(ctx context.Context) error { + // First, get direct dependencies from go list -m + directCmd := "go list -m -json all" + directResult, err := gproc.ShellExec(ctx, directCmd) + if err != nil { + return fmt.Errorf("failed to execute go list -m: %v", err) + } + // Parse direct dependencies + directDecoder := json.NewDecoder(strings.NewReader(directResult)) + for directDecoder.More() { + var mod struct { + Path string `json:"Path"` + Version string `json:"Version"` + Main bool `json:"Main"` + Indirect bool `json:"Indirect"` + } + if err := directDecoder.Decode(&mod); err != nil { + continue + } + + // Skip main module + if mod.Main { + continue + } + + // Track direct vs indirect + if !mod.Indirect { + a.directModules[mod.Path] = true + } + + // Create module info + a.modules[mod.Path] = &ModuleInfo{ + Path: mod.Path, + Version: mod.Version, + IsDirect: !mod.Indirect, + Deps: []string{}, + } + } + + // Then, get module dependency graph + graphCmd := "go mod graph" + graphResult, err := gproc.ShellExec(ctx, graphCmd) + if err != nil { + return fmt.Errorf("failed to execute go mod graph: %v", err) + } + + // Parse go mod graph output + // Format: module1@version module2@version + lines := gstr.Split(gstr.Trim(graphResult), "\n") + for _, line := range lines { + line = gstr.Trim(line) + if line == "" { + continue + } + + parts := gstr.Split(line, " ") + if len(parts) != 2 { + continue + } + + from := a.parseModulePath(parts[0]) + to := a.parseModulePath(parts[1]) + + // Add to graph + a.moduleGraph[from] = append(a.moduleGraph[from], to) + + // Ensure both modules exist in our map + if _, ok := a.modules[from]; !ok { + a.modules[from] = &ModuleInfo{ + Path: from, + Version: a.parseModuleVersion(parts[0]), + IsDirect: a.directModules[from], + } + } + if _, ok := a.modules[to]; !ok { + a.modules[to] = &ModuleInfo{ + Path: to, + Version: a.parseModuleVersion(parts[1]), + IsDirect: a.directModules[to], + } + } + + // Update deps + a.modules[from].Deps = append(a.modules[from].Deps, to) + } + + return nil +} + +// parseModulePath extracts module path from "module@version" format. +func (a *analyzer) parseModulePath(s string) string { + if idx := gstr.Pos(s, "@"); idx > 0 { + return s[:idx] + } + return s +} + +// parseModuleVersion extracts version from "module@version" format. +func (a *analyzer) parseModuleVersion(s string) string { + if idx := gstr.Pos(s, "@"); idx > 0 { + return s[idx+1:] + } + return "" +} + +// generateModuleOutput generates output for module-level dependencies. +func (a *analyzer) generateModuleOutput(in Input) string { + switch in.Format { + case "tree": + return a.generateModuleTree(in) + case "list": + return a.generateModuleList(in) + case "mermaid": + return a.generateModuleMermaid(in) + case "dot": + return a.generateModuleDot(in) + case "json": + return a.generateModuleJSON(in) + default: + return a.generateModuleTree(in) + } +} + +// generateModuleTree generates tree output for module dependencies. +func (a *analyzer) generateModuleTree(in Input) string { + var sb strings.Builder + + // Get modules to display + modules := a.getFilteredModules(in) + + sb.WriteString(fmt.Sprintf("%s (module dependencies)\n", a.modulePrefix)) + + // Show direct dependencies first + directDeps := make([]string, 0) + for _, mod := range modules { + if mod.IsDirect { + directDeps = append(directDeps, mod.Path) + } + } + sort.Strings(directDeps) + + visited := make(map[string]bool) + for i, dep := range directDeps { + isLast := i == len(directDeps)-1 + a.printModuleTreeNode(&sb, dep, "", isLast, in, visited, 0) + } + + return sb.String() +} + +// printModuleTreeNode prints a module tree node. +func (a *analyzer) printModuleTreeNode(sb *strings.Builder, modPath string, prefix string, isLast bool, in Input, visited map[string]bool, depth int) { + if in.Depth > 0 && depth >= in.Depth { + return + } + + connector := "├── " + if isLast { + connector = "└── " + } + + mod := a.modules[modPath] + version := "" + if mod != nil && mod.Version != "" { + version = "@" + mod.Version + } + + sb.WriteString(prefix + connector + modPath + version + "\n") + + // Skip if Direct mode and this is not a direct dependency + if in.Direct { + return + } + + // Check for cycles + if visited[modPath] { + return + } + visited[modPath] = true + + // Get dependencies + deps := a.moduleGraph[modPath] + sort.Strings(deps) + + newPrefix := prefix + if isLast { + newPrefix += " " + } else { + newPrefix += "│ " + } + + for i, dep := range deps { + depIsLast := i == len(deps)-1 + a.printModuleTreeNode(sb, dep, newPrefix, depIsLast, in, visited, depth+1) + } + + delete(visited, modPath) +} + +// generateModuleList generates list output for module dependencies. +func (a *analyzer) generateModuleList(in Input) string { + var sb strings.Builder + + modules := a.getFilteredModules(in) + + // Count stats + directCount := 0 + indirectCount := 0 + for _, mod := range modules { + if mod.IsDirect { + directCount++ + } else { + indirectCount++ + } + } + + sb.WriteString(fmt.Sprintf("# Module Dependencies for %s\n", a.modulePrefix)) + sb.WriteString(fmt.Sprintf("# Direct: %d, Indirect: %d, Total: %d\n\n", directCount, indirectCount, len(modules))) + + // Sort by path + paths := make([]string, 0, len(modules)) + for path := range modules { + paths = append(paths, path) + } + sort.Strings(paths) + + for _, path := range paths { + mod := modules[path] + marker := "" + if mod.IsDirect { + marker = " (direct)" + } + sb.WriteString(fmt.Sprintf("%s@%s%s\n", mod.Path, mod.Version, marker)) + } + + return sb.String() +} + +// generateModuleMermaid generates Mermaid diagram for module dependencies. +func (a *analyzer) generateModuleMermaid(in Input) string { + var sb strings.Builder + sb.WriteString("```mermaid\n") + sb.WriteString("graph TD\n") + + edges := a.collectModuleEdges(in) + sortedEdges := make([]string, 0, len(edges)) + for edge := range edges { + sortedEdges = append(sortedEdges, edge) + } + sort.Strings(sortedEdges) + + for _, edge := range sortedEdges { + sb.WriteString(" " + edge + "\n") + } + sb.WriteString("```\n") + return sb.String() +} + +// generateModuleDot generates Graphviz DOT for module dependencies. +func (a *analyzer) generateModuleDot(in Input) string { + var sb strings.Builder + sb.WriteString("digraph modules {\n") + sb.WriteString(" rankdir=TB;\n") + sb.WriteString(" node [shape=box];\n") + + edges := a.collectModuleEdges(in) + sortedEdges := make([]string, 0, len(edges)) + for edge := range edges { + sortedEdges = append(sortedEdges, edge) + } + sort.Strings(sortedEdges) + + for _, edge := range sortedEdges { + parts := gstr.Split(edge, " --> ") + if len(parts) == 2 { + fmt.Fprintf(&sb, " \"%s\" -> \"%s\";\n", parts[0], parts[1]) + } + } + sb.WriteString("}\n") + return sb.String() +} + +// generateModuleJSON generates JSON output for module dependencies. +func (a *analyzer) generateModuleJSON(in Input) string { + modules := a.getFilteredModules(in) + + result := make(map[string]any) + + // Build module list + moduleList := make([]map[string]any, 0) + for _, mod := range modules { + m := map[string]any{ + "path": mod.Path, + "version": mod.Version, + "direct": mod.IsDirect, + "depCount": len(mod.Deps), + } + if !in.Direct { + m["dependencies"] = mod.Deps + } + moduleList = append(moduleList, m) + } + + // Sort by path + sort.Slice(moduleList, func(i, j int) bool { + return moduleList[i]["path"].(string) < moduleList[j]["path"].(string) + }) + + result["modules"] = moduleList + + // Add statistics + directCount := 0 + indirectCount := 0 + for _, mod := range modules { + if mod.IsDirect { + directCount++ + } else { + indirectCount++ + } + } + result["statistics"] = map[string]any{ + "total": len(modules), + "direct": directCount, + "indirect": indirectCount, + } + + result["metadata"] = map[string]any{ + "module": a.modulePrefix, + "format": in.Format, + "depth": in.Depth, + "direct": in.Direct, + } + + data, err := json.MarshalIndent(result, "", " ") + if err != nil { + return fmt.Sprintf("Error: %v", err) + } + return string(data) +} + +// getFilteredModules returns modules based on filter options. +func (a *analyzer) getFilteredModules(in Input) map[string]*ModuleInfo { + result := make(map[string]*ModuleInfo) + + for path, mod := range a.modules { + // Skip main module + if path == a.modulePrefix { + continue + } + + // Filter by direct/indirect + if in.Direct && !mod.IsDirect { + continue + } + + result[path] = mod + } + + return result +} + +// collectModuleEdges collects edges for module dependency graph. +func (a *analyzer) collectModuleEdges(in Input) map[string]bool { + edges := make(map[string]bool) + + // Start from main module + if in.Direct { + // Only show direct dependencies from main module + for path, mod := range a.modules { + if mod.IsDirect { + from := a.sanitizeName(a.shortModuleName(a.modulePrefix)) + to := a.sanitizeName(a.shortModuleName(path)) + edge := fmt.Sprintf("%s --> %s", from, to) + edges[edge] = true + } + } + } else { + // Show full dependency graph + visited := make(map[string]bool) + a.collectModuleEdgesRecursive(a.modulePrefix, in, edges, visited, 0) + } + + return edges +} + +// collectModuleEdgesRecursive recursively collects module edges. +func (a *analyzer) collectModuleEdgesRecursive(modPath string, in Input, edges map[string]bool, visited map[string]bool, depth int) { + if in.Depth > 0 && depth >= in.Depth { + return + } + + if visited[modPath] { + return + } + visited[modPath] = true + + deps := a.moduleGraph[modPath] + fromName := a.sanitizeName(a.shortModuleName(modPath)) + + for _, dep := range deps { + toName := a.sanitizeName(a.shortModuleName(dep)) + if fromName != toName { + edge := fmt.Sprintf("%s --> %s", fromName, toName) + edges[edge] = true + } + a.collectModuleEdgesRecursive(dep, in, edges, visited, depth+1) + } +} + +// shortModuleName returns the module name (keeping full domain for clarity). +func (a *analyzer) shortModuleName(modPath string) string { + // Return full module path to preserve domain information + return modPath +} // convertInputToFilterOptions converts legacy Input to new FilterOptions. func (a *analyzer) convertInputToFilterOptions(in Input) *FilterOptions { @@ -228,7 +657,6 @@ func (a *analyzer) convertInputToFilterOptions(in Input) *FilterOptions { IncludeInternal: in.Internal, IncludeExternal: in.External, IncludeStdLib: !in.NoStd, - MainModuleOnly: in.MainOnly, Depth: in.Depth, } @@ -321,11 +749,6 @@ func (opts *FilterOptions) Normalize(modulePrefix string) error { // ShouldInclude determines if a package should be included based on filter options. func (opts *FilterOptions) ShouldInclude(pkg *PackageInfo) bool { - // Filter by main module only (exclude submodules) - if opts.MainModuleOnly && !pkg.IsMainModule { - return false - } - // Filter by kind switch pkg.Kind { case KindStdLib: @@ -383,36 +806,6 @@ func (tc *TraversalContext) GetDependencies(pkg string) []string { return result } -// isMainModulePackage checks if a package belongs to the main module (not a submodule). -func (a *analyzer) isMainModulePackage(pkg string) bool { - if a.modulePrefix == "" { - return true // If no module prefix, consider all as main module - } - - if !gstr.HasPrefix(pkg, a.modulePrefix) { - return false // Not even in our module - } - - // Remove the module prefix to get the relative path - relativePath := gstr.TrimLeft(pkg[len(a.modulePrefix):], "/") - if relativePath == "" { - return true // This is the root module itself - } - - // Check if this path contains a go.mod file (indicating a submodule) - // We check from the most specific path up to the root - parts := gstr.Split(relativePath, "/") - for i := len(parts); i > 0; i-- { - subPath := gstr.Join(parts[:i], "/") - if subPath != "" && gfile.Exists(subPath+"/go.mod") { - // Found a go.mod file in a subdirectory, this indicates a submodule - return false - } - } - - return true // This is part of the main module -} - // shortName returns a shortened package name. func (a *analyzer) shortName(pkg string, group bool) string { if a.modulePrefix != "" && gstr.HasPrefix(pkg, a.modulePrefix) { @@ -534,7 +927,6 @@ func (a *analyzer) getDependencyStats(_ Input) map[string]any { IncludeInternal: true, IncludeExternal: true, IncludeStdLib: true, - MainModuleOnly: false, Depth: 0, } @@ -639,11 +1031,10 @@ func (a *analyzer) buildPackageStore() *PackageStore { // Convert go packages to PackageInfo for path, goPkg := range a.packages { pkgInfo := &PackageInfo{ - ImportPath: path, - ModulePath: goPkg.Module.Path, - IsStdLib: goPkg.Standard, - Imports: goPkg.Imports, - IsMainModule: a.isMainModulePackage(path), + ImportPath: path, + ModulePath: goPkg.Module.Path, + IsStdLib: goPkg.Standard, + Imports: goPkg.Imports, } pkgInfo.Kind = store.identifyPackageKind(pkgInfo) store.packages[path] = pkgInfo diff --git a/cmd/gf/internal/cmd/cmddep/cmddep_external_test.go b/cmd/gf/internal/cmd/cmddep/cmddep_external_test.go index d90680b18..b62718fa8 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep_external_test.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep_external_test.go @@ -16,22 +16,46 @@ func TestExternalDependencyAnalysis(t *testing.T) { gtest.C(t, func(t *gtest.T) { analyzer := newAnalyzer() analyzer.modulePrefix = "github.com/gogf/gf/cmd/gf/v2" + analyzer.packages = map[string]*goPackage{ + "github.com/other/package": { + ImportPath: "github.com/other/package", + Standard: false, + }, + "github.com/gogf/gf/cmd/gf/v2/internal": { + ImportPath: "github.com/gogf/gf/cmd/gf/v2/internal", + Standard: false, + }, + "fmt": { + ImportPath: "fmt", + Standard: true, + }, + } - // Test shouldInclude with external dependencies + // Test using new FilterOptions system in := Input{ Internal: false, External: true, NoStd: true, } + opts := analyzer.convertInputToFilterOptions(in) + opts.Normalize(analyzer.modulePrefix) + store := analyzer.buildPackageStore() + // Test external package (should be included) - t.Assert(analyzer.shouldInclude("github.com/other/package", in), true) + externalPkg, ok := store.packages["github.com/other/package"] + t.Assert(ok, true) + t.Assert(opts.ShouldInclude(externalPkg), true) // Test internal package (should not be included) - t.Assert(analyzer.shouldInclude("github.com/gogf/gf/cmd/gf/v2/internal", in), false) + internalPkg, ok := store.packages["github.com/gogf/gf/cmd/gf/v2/internal"] + t.Assert(ok, true) + t.Assert(opts.ShouldInclude(internalPkg), false) // Test standard library (should not be included due to NoStd) - t.Assert(analyzer.shouldInclude("fmt", in), false) + stdPkg, ok := store.packages["fmt"] + t.Assert(ok, true) + t.Assert(opts.ShouldInclude(stdPkg), false) }) } @@ -39,11 +63,11 @@ func TestExternalGrouping(t *testing.T) { gtest.C(t, func(t *gtest.T) { analyzer := newAnalyzer() - // Test external group extraction - t.Assert(analyzer.getExternalGroup("github.com/user/repo"), "github.com/user") - t.Assert(analyzer.getExternalGroup("golang.org/x/tools"), "golang.org") - t.Assert(analyzer.getExternalGroup("fmt"), "stdlib") - t.Assert(analyzer.getExternalGroup("simple"), "simple") + // Test external group extraction using shortName + t.Assert(analyzer.shortName("github.com/user/repo", false), "github.com/user/repo") + t.Assert(analyzer.shortName("golang.org/x/tools", false), "golang.org/x/tools") + t.Assert(analyzer.shortName("fmt", false), "fmt") + t.Assert(analyzer.shortName("simple", false), "simple") }) } diff --git a/cmd/gf/internal/cmd/cmddep/cmddep_output.go b/cmd/gf/internal/cmd/cmddep/cmddep_output.go index 2da788ec6..17222ec14 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep_output.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep_output.go @@ -306,7 +306,6 @@ func (a *analyzer) generateJSON(in Input) string { "internal": in.Internal, "external": in.External, "nostd": in.NoStd, - "main": in.MainOnly, } data, err := json.MarshalIndent(result, "", " ") diff --git a/cmd/gf/internal/cmd/cmddep/cmddep_server.go b/cmd/gf/internal/cmd/cmddep/cmddep_server.go index e14300754..78301f3ea 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep_server.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep_server.go @@ -7,6 +7,7 @@ package cmddep import ( + "context" "embed" "encoding/json" "fmt" @@ -177,11 +178,30 @@ func (a *analyzer) handleGraphAPI(w http.ResponseWriter, r *http.Request, in Inp if e := query.Get("external"); e != "" { in.External = e == "true" } - if m := query.Get("main"); m != "" { - in.MainOnly = m == "true" + if m := query.Get("module"); m != "" { + in.Module = m == "true" + } + if d := query.Get("direct"); d != "" { + in.Direct = d == "true" } pkg := query.Get("package") + // Module-level graph + if in.Module { + // Ensure module graph is loaded + if len(a.modules) == 0 { + if err := a.loadModuleGraph(context.Background()); err != nil { + http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError) + return + } + } + + data := a.buildModuleGraphData(in) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) + return + } + var data *graphData if pkg != "" { data = a.buildPackageGraphData(pkg, in) @@ -192,7 +212,7 @@ func (a *analyzer) handleGraphAPI(w http.ResponseWriter, r *http.Request, in Inp json.NewEncoder(w).Encode(data) } -// handlePackagesAPI returns all packages list with dependency stats. +// handlePackagesAPI returns all packages list with dependency stats using new system. func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in Input) { query := r.URL.Query() if i := query.Get("internal"); i != "" { @@ -201,31 +221,48 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in if e := query.Get("external"); e != "" { in.External = e == "true" } - if m := query.Get("main"); m != "" { - in.MainOnly = m == "true" + if m := query.Get("module"); m != "" { + in.Module = m == "true" } + if d := query.Get("direct"); d != "" { + in.Direct = d == "true" + } + + // Module-level packages + if in.Module { + a.handleModulesAPI(w, in) + return + } + + opts := a.convertInputToFilterOptions(in) + opts.Normalize(a.modulePrefix) + store := a.buildPackageStore() // Build reverse dependency map (who uses each package) usedByMap := make(map[string]int) - for fullPath, pkg := range a.packages { - if !a.shouldInclude(fullPath, in) { + for fullPath, pkgInfo := range store.packages { + if !opts.ShouldInclude(pkgInfo) { continue } fromShort := a.shortName(fullPath, false) if fromShort == "" { continue } - for _, dep := range a.filterDeps(pkg.Imports, in) { - shortDep := a.shortName(dep, false) - if shortDep != "" { - usedByMap[shortDep]++ + for _, dep := range pkgInfo.Imports { + depInfo, ok := store.packages[dep] + if ok && opts.ShouldInclude(depInfo) { + shortDep := a.shortName(dep, false) + if shortDep != "" { + usedByMap[shortDep]++ + } } } } packages := make([]packageSummary, 0) for _, pkgPath := range a.getSortedPackages() { - if !a.shouldInclude(pkgPath, in) { + pkgInfo, ok := store.packages[pkgPath] + if !ok || !opts.ShouldInclude(pkgInfo) { continue } shortName := a.shortName(pkgPath, false) @@ -235,8 +272,9 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in // Count dependencies (filtered) depCount := 0 - if pkg, ok := a.packages[pkgPath]; ok { - for _, dep := range a.filterDeps(pkg.Imports, in) { + for _, dep := range pkgInfo.Imports { + depInfo, ok := store.packages[dep] + if ok && opts.ShouldInclude(depInfo) { shortDep := a.shortName(dep, false) if shortDep != "" { depCount++ @@ -261,7 +299,7 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in json.NewEncoder(w).Encode(result) } -// handlePackageAPI returns detailed info for a specific package. +// handlePackageAPI returns detailed info for a specific package using new system. func (a *analyzer) handlePackageAPI(w http.ResponseWriter, r *http.Request, in Input) { query := r.URL.Query() pkgName := query.Get("name") @@ -284,7 +322,16 @@ func (a *analyzer) handlePackageAPI(w http.ResponseWriter, r *http.Request, in I return } - pkg := a.packages[fullPath] + opts := a.convertInputToFilterOptions(in) + opts.Normalize(a.modulePrefix) + store := a.buildPackageStore() + + pkgInfo, ok := store.packages[fullPath] + if !ok { + http.Error(w, "package not found", http.StatusNotFound) + return + } + info := packageInfo{ Name: pkgName, FullPath: fullPath, @@ -293,18 +340,21 @@ func (a *analyzer) handlePackageAPI(w http.ResponseWriter, r *http.Request, in I } // Get dependencies - for _, dep := range a.filterDeps(pkg.Imports, in) { - shortName := a.shortName(dep, false) - if shortName != "" { - info.Dependencies = append(info.Dependencies, shortName) + for _, dep := range pkgInfo.Imports { + depInfo, ok := store.packages[dep] + if ok && opts.ShouldInclude(depInfo) { + shortName := a.shortName(dep, false) + if shortName != "" { + info.Dependencies = append(info.Dependencies, shortName) + } } } sort.Strings(info.Dependencies) // Get reverse dependencies (who uses this package) - for path, p := range a.packages { + for path, p := range store.packages { for _, dep := range p.Imports { - if dep == fullPath { + if dep == fullPath && opts.ShouldInclude(p) { shortName := a.shortName(path, false) if shortName != "" { info.UsedBy = append(info.UsedBy, shortName) @@ -331,11 +381,30 @@ func (a *analyzer) handleTreeAPI(w http.ResponseWriter, r *http.Request, in Inpu if e := query.Get("external"); e != "" { in.External = e == "true" } - if m := query.Get("main"); m != "" { - in.MainOnly = m == "true" + if m := query.Get("module"); m != "" { + in.Module = m == "true" + } + if d := query.Get("direct"); d != "" { + in.Direct = d == "true" } pkg := query.Get("package") + // Module-level tree + if in.Module { + // Ensure module graph is loaded + if len(a.modules) == 0 { + if err := a.loadModuleGraph(context.Background()); err != nil { + http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError) + return + } + } + + output := a.generateModuleTree(in) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write([]byte(output)) + return + } + var output string if pkg != "" { output = a.generatePackageTree(pkg, in) @@ -356,11 +425,30 @@ func (a *analyzer) handleListAPI(w http.ResponseWriter, r *http.Request, in Inpu if e := query.Get("external"); e != "" { in.External = e == "true" } - if m := query.Get("main"); m != "" { - in.MainOnly = m == "true" + if m := query.Get("module"); m != "" { + in.Module = m == "true" + } + if d := query.Get("direct"); d != "" { + in.Direct = d == "true" } pkg := query.Get("package") + // Module-level list + if in.Module { + // Ensure module graph is loaded + if len(a.modules) == 0 { + if err := a.loadModuleGraph(context.Background()); err != nil { + http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError) + return + } + } + + output := a.generateModuleList(in) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write([]byte(output)) + return + } + var output string if pkg != "" { output = a.generatePackageList(pkg, in) @@ -412,7 +500,7 @@ func (a *analyzer) buildGraphData(in Input) *graphData { return data } -// buildPackageGraphData builds graph data for a specific package. +// buildPackageGraphData builds graph data for a specific package using new system. func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData { data := &graphData{ Nodes: make([]graphNode, 0), @@ -432,6 +520,15 @@ func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData { return data } + opts := a.convertInputToFilterOptions(in) + opts.Normalize(a.modulePrefix) + store := a.buildPackageStore() + + _, ok := store.packages[fullPath] + if !ok { + return data + } + nodeSet := make(map[string]bool) nodeSet[pkgName] = true data.Nodes = append(data.Nodes, graphNode{ @@ -444,9 +541,9 @@ func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData { if in.Reverse { // Show packages that depend on this package - for path, p := range a.packages { + for path, p := range store.packages { for _, dep := range p.Imports { - if dep == fullPath { + if dep == fullPath && opts.ShouldInclude(p) { shortName := a.shortName(path, false) if shortName != "" && !nodeSet[shortName] { nodeSet[shortName] = true @@ -472,14 +569,27 @@ func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData { return data } -// collectPackageDeps recursively collects dependencies for a package. +// collectPackageDeps recursively collects dependencies for a package using new system. func (a *analyzer) collectPackageDeps(pkg *goPackage, pkgName string, in Input, nodeSet map[string]bool, data *graphData, depth int) { if in.Depth > 0 && depth >= in.Depth { return } - deps := a.filterDeps(pkg.Imports, in) - for _, dep := range deps { + opts := a.convertInputToFilterOptions(in) + opts.Normalize(a.modulePrefix) + store := a.buildPackageStore() + + pkgInfo, ok := store.packages[pkg.ImportPath] + if !ok { + return + } + + for _, dep := range pkgInfo.Imports { + depInfo, ok := store.packages[dep] + if !ok || !opts.ShouldInclude(depInfo) { + continue + } + shortName := a.shortName(dep, false) if shortName == "" { continue @@ -506,7 +616,7 @@ func (a *analyzer) collectPackageDeps(pkg *goPackage, pkgName string, in Input, } } -// generatePackageTree generates tree output for a specific package. +// generatePackageTree generates tree output for a specific package using new system. func (a *analyzer) generatePackageTree(pkgName string, in Input) string { var fullPath string for path := range a.packages { @@ -520,15 +630,29 @@ func (a *analyzer) generatePackageTree(pkgName string, in Input) string { return "Package not found: " + pkgName } + opts := a.convertInputToFilterOptions(in) + opts.Normalize(a.modulePrefix) + store := a.buildPackageStore() + + _, ok := store.packages[fullPath] + if !ok { + return "Package not found: " + pkgName + } + var sb strings.Builder - pkg := a.packages[fullPath] - a.visited = make(map[string]bool) + ctx := &TraversalContext{ + visited: make(map[string]bool), + options: opts, + store: store, + maxDepth: opts.Depth, + } + sb.WriteString(pkgName + "\n") - a.printTreeNode(&sb, pkg, "", in, 0) + a.printTreeNodeNew(&sb, fullPath, "", in, ctx, 0) return sb.String() } -// generatePackageList generates list output for a specific package. +// generatePackageList generates list output for a specific package using new system. func (a *analyzer) generatePackageList(pkgName string, in Input) string { var fullPath string for path := range a.packages { @@ -542,15 +666,25 @@ func (a *analyzer) generatePackageList(pkgName string, in Input) string { return "Package not found: " + pkgName } - var sb strings.Builder - pkg := a.packages[fullPath] - deps := a.filterDeps(pkg.Imports, in) + opts := a.convertInputToFilterOptions(in) + opts.Normalize(a.modulePrefix) + store := a.buildPackageStore() - shortDeps := make([]string, 0, len(deps)) - for _, dep := range deps { - shortName := a.shortName(dep, false) - if shortName != "" { - shortDeps = append(shortDeps, shortName) + pkgInfo, ok := store.packages[fullPath] + if !ok { + return "Package not found: " + pkgName + } + + var sb strings.Builder + + shortDeps := make([]string, 0) + for _, dep := range pkgInfo.Imports { + depInfo, ok := store.packages[dep] + if ok && opts.ShouldInclude(depInfo) { + shortName := a.shortName(dep, false) + if shortName != "" { + shortDeps = append(shortDeps, shortName) + } } } sort.Strings(shortDeps) @@ -743,6 +877,94 @@ func (s *serverState) handleAnalyzeAPI(w http.ResponseWriter, r *http.Request) { return } + // IMPORTANT: Load module dependencies from the actual module directory + // We need to run commands in moduleDir context to get the correct direct/indirect info + // Running in tempDir would show everything as indirect since we're not using them there + + // IMPORTANT: Load direct dependencies info FIRST + // This must be done before parsing go mod graph so that IsDirect flag is correct + listModCmd := exec.Command("go", "list", "-m", "-json", "all") + listModCmd.Dir = moduleDir // Run in the actual module directory + modOutput, err := listModCmd.CombinedOutput() + if err == nil { + modDecoder := json.NewDecoder(strings.NewReader(string(modOutput))) + for modDecoder.More() { + var mod struct { + Path string `json:"Path"` + Version string `json:"Version"` + Main bool `json:"Main"` + Indirect bool `json:"Indirect"` + } + if err := modDecoder.Decode(&mod); err != nil { + continue + } + + // Skip main module + if mod.Main { + continue + } + + // Track direct vs indirect + if !mod.Indirect { + newAnalyzer.directModules[mod.Path] = true + } + + // Create module info + newAnalyzer.modules[mod.Path] = &ModuleInfo{ + Path: mod.Path, + Version: mod.Version, + IsDirect: !mod.Indirect, + Deps: []string{}, + } + } + } + + // Load module-level dependencies for the remote module + // Run go mod graph in the module directory context + graphCmd := exec.Command("go", "mod", "graph") + graphCmd.Dir = moduleDir // Run in the actual module directory + graphOutput, err := graphCmd.CombinedOutput() + if err == nil { + // Parse module graph + lines := strings.Split(strings.TrimSpace(string(graphOutput)), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + parts := strings.Fields(line) + if len(parts) != 2 { + continue + } + + from := newAnalyzer.parseModulePath(parts[0]) + to := newAnalyzer.parseModulePath(parts[1]) + + // Add to graph + newAnalyzer.moduleGraph[from] = append(newAnalyzer.moduleGraph[from], to) + + // Ensure both modules exist in our map (use existing or create with correct IsDirect) + if _, ok := newAnalyzer.modules[from]; !ok { + newAnalyzer.modules[from] = &ModuleInfo{ + Path: from, + Version: newAnalyzer.parseModuleVersion(parts[0]), + IsDirect: newAnalyzer.directModules[from], + } + } + if _, ok := newAnalyzer.modules[to]; !ok { + newAnalyzer.modules[to] = &ModuleInfo{ + Path: to, + Version: newAnalyzer.parseModuleVersion(parts[1]), + IsDirect: newAnalyzer.directModules[to], + } + } + + // Update deps + newAnalyzer.modules[from].Deps = append(newAnalyzer.modules[from].Deps, to) + } + } + s.currentAnalyzer = newAnalyzer w.Header().Set("Content-Type", "application/json") @@ -819,3 +1041,105 @@ func (s *serverState) handleResetAPI(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"success": true}) } + +// handleModulesAPI returns module-level dependency list. +func (a *analyzer) handleModulesAPI(w http.ResponseWriter, in Input) { + // Ensure module graph is loaded + if len(a.modules) == 0 { + if err := a.loadModuleGraph(context.Background()); err != nil { + http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError) + return + } + } + + modules := a.getFilteredModules(in) + + // Build module list + moduleList := make([]map[string]any, 0) + for _, mod := range modules { + m := map[string]any{ + "name": mod.Path, + "depCount": len(mod.Deps), + "usedByCount": 0, // TODO: calculate reverse deps + "version": mod.Version, + "direct": mod.IsDirect, + } + moduleList = append(moduleList, m) + } + + // Sort by path + sort.Slice(moduleList, func(i, j int) bool { + return moduleList[i]["name"].(string) < moduleList[j]["name"].(string) + }) + + // Count stats + directCount := 0 + indirectCount := 0 + for _, mod := range modules { + if mod.IsDirect { + directCount++ + } else { + indirectCount++ + } + } + + result := map[string]any{ + "packages": moduleList, + "statistics": map[string]any{ + "total": len(modules), + "direct": directCount, + "indirect": indirectCount, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// buildModuleGraphData builds graph data for module-level visualization. +func (a *analyzer) buildModuleGraphData(in Input) *graphData { + data := &graphData{ + Nodes: make([]graphNode, 0), + Edges: make([]graphEdge, 0), + } + + nodeSet := make(map[string]bool) + edges := a.collectModuleEdges(in) + + for edge := range edges { + parts := strings.Split(edge, " --> ") + if len(parts) != 2 { + continue + } + from, to := parts[0], parts[1] + + if !nodeSet[from] { + nodeSet[from] = true + group := "indirect" + if mod, ok := a.modules[from]; ok && mod.IsDirect { + group = "direct" + } + data.Nodes = append(data.Nodes, graphNode{ + ID: from, + Label: strings.ReplaceAll(from, "_", "/"), + Group: group, + }) + } + if !nodeSet[to] { + nodeSet[to] = true + group := "indirect" + if mod, ok := a.modules[to]; ok && mod.IsDirect { + group = "direct" + } + data.Nodes = append(data.Nodes, graphNode{ + ID: to, + Label: strings.ReplaceAll(to, "_", "/"), + Group: group, + }) + } + + data.Edges = append(data.Edges, graphEdge{From: from, To: to}) + } + + return data +} diff --git a/cmd/gf/internal/cmd/cmddep/cmddep_z_unit_test.go b/cmd/gf/internal/cmd/cmddep/cmddep_z_unit_test.go index 454252da2..50afeb108 100644 --- a/cmd/gf/internal/cmd/cmddep/cmddep_z_unit_test.go +++ b/cmd/gf/internal/cmd/cmddep/cmddep_z_unit_test.go @@ -13,6 +13,129 @@ import ( "github.com/gogf/gf/v2/test/gtest" ) +// Test data model creation and classification +func Test_PackageInfo_Creation(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + pkg := &PackageInfo{ + ImportPath: "github.com/gogf/gf/v2/os/gfile", + ModulePath: "github.com/gogf/gf/v2", + Kind: KindInternal, + Tier: 2, + Imports: []string{"fmt", "os"}, + IsStdLib: false, + IsModuleRoot: false, + } + t.Assert(pkg != nil, true) + t.Assert(pkg.Kind, KindInternal) + t.Assert(pkg.Tier, 2) + t.Assert(len(pkg.Imports), 2) + }) +} + +// Test FilterOptions normalization +func Test_FilterOptions_Normalize(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + opts := &FilterOptions{ + IncludeInternal: false, + IncludeExternal: false, + IncludeStdLib: false, + } + opts.Normalize("github.com/gogf/gf/v2") + + // After normalization, internal should be included by default + t.Assert(opts.IncludeInternal, true) + t.Assert(opts.IncludeExternal, false) + }) +} + +// Test ShouldInclude decision logic +func Test_FilterOptions_ShouldInclude(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test internal package inclusion + opts := &FilterOptions{ + IncludeInternal: true, + IncludeExternal: false, + IncludeStdLib: true, + } + + internalPkg := &PackageInfo{ + Kind: KindInternal, + IsStdLib: false, + } + externalPkg := &PackageInfo{ + Kind: KindExternal, + IsStdLib: false, + } + stdlibPkg := &PackageInfo{ + Kind: KindStdLib, + IsStdLib: true, + } + + t.Assert(opts.ShouldInclude(internalPkg), true) + t.Assert(opts.ShouldInclude(externalPkg), false) + t.Assert(opts.ShouldInclude(stdlibPkg), true) + }) +} + +// Test TraversalContext Visit tracking +func Test_TraversalContext_Visit(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + ctx := &TraversalContext{ + visited: make(map[string]bool), + } + + // First visit should return false + t.Assert(ctx.Visit("pkg1"), false) + // Second visit should return true + t.Assert(ctx.Visit("pkg1"), true) + // New package should return false + t.Assert(ctx.Visit("pkg2"), false) + }) +} + +// Test guessModuleRoot function +func Test_GuessModuleRoot(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + tests := []struct { + input string + expect string + }{ + {"github.com/gogf/gf", "github.com/gogf/gf"}, + {"github.com/gogf/gf/v2", "github.com/gogf/gf/v2"}, + {"github.com/gogf/gf/v2/os/gfile", "github.com/gogf/gf/v2"}, + {"github.com/gogf/gf/v2/os", "github.com/gogf/gf/v2"}, + } + + for _, test := range tests { + result := guessModuleRoot(test.input) + t.AssertEQ(result, test.expect) + } + }) +} + +// Test input to filter options conversion +func Test_ConvertInputToFilterOptions(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + a := newAnalyzer() + + input := Input{ + Internal: true, + External: false, + NoStd: true, + Module: false, + Direct: false, + Depth: 3, + } + + opts := a.convertInputToFilterOptions(input) + + t.Assert(opts.IncludeInternal, true) + t.Assert(opts.IncludeExternal, false) + t.Assert(opts.IncludeStdLib, false) // NoStd=true means !IncludeStdLib + t.Assert(opts.Depth, 3) + }) +} + func Test_Dep_Tree(t *testing.T) { gtest.C(t, func(t *gtest.T) { ctx := gctx.New() @@ -112,3 +235,21 @@ func Test_Dep_Group(t *testing.T) { t.AssertNil(err) }) } + +func Test_ModuleLevel_Direct(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + ctx := gctx.New() + + // Test module level with direct only + in := Input{ + Module: true, + Direct: true, + Format: "list", + } + + t.Logf("Input.Module: %v, Input.Direct: %v", in.Module, in.Direct) + + _, err := Dep.Index(ctx, in) + t.AssertNil(err) + }) +} diff --git a/cmd/gf/internal/cmd/cmddep/static/app.js b/cmd/gf/internal/cmd/cmddep/static/app.js index f6a322a5b..7958216e9 100644 --- a/cmd/gf/internal/cmd/cmddep/static/app.js +++ b/cmd/gf/internal/cmd/cmddep/static/app.js @@ -123,7 +123,11 @@ async function fetchVersions() { const versionSelect = document.getElementById('versionSelect'); const spinner = document.getElementById('loadingSpinner'); - const modulePath = input.value.trim(); + let modulePath = input.value.trim(); + // Remove http:// or https:// prefix if present + modulePath = modulePath.replace(/^https?:\/\//, ''); + input.value = modulePath; + if (!modulePath) { versionSelect.disabled = true; versionSelect.innerHTML = ``; @@ -163,7 +167,11 @@ async function analyzeRemoteModule() { const spinner = document.getElementById('loadingSpinner'); const analyzeBtn = document.getElementById('analyzeBtn'); - const modulePath = input.value.trim(); + let modulePath = input.value.trim(); + // Remove http:// or https:// prefix if present + modulePath = modulePath.replace(/^https?:\/\//, ''); + input.value = modulePath; + const version = versionSelect.value; if (!modulePath) { @@ -410,8 +418,9 @@ async function loadPackages() { try { const internal = document.getElementById('internal').checked; const external = document.getElementById('external') ? document.getElementById('external').checked : false; - const mainOnly = document.getElementById('mainOnly') ? document.getElementById('mainOnly').checked : false; - const response = await fetch(`/api/packages?internal=${internal}&external=${external}&main=${mainOnly}`); + const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false; + const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false; + const response = await fetch(`/api/packages?internal=${internal}&external=${external}&module=${moduleLevel}&direct=${directOnly}`); const data = await response.json(); // Handle new API response format with packages and statistics @@ -678,7 +687,8 @@ async function refresh() { const reverse = document.getElementById('reverse').checked; const internal = document.getElementById('internal').checked; const external = document.getElementById('external') ? document.getElementById('external').checked : false; - const mainOnly = document.getElementById('mainOnly') ? document.getElementById('mainOnly').checked : false; + const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false; + const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false; if (selectedPackage) { await showPackageInfo(selectedPackage); @@ -687,11 +697,11 @@ async function refresh() { } if (currentView === 'graph') { - await refreshGraph(depth, group, reverse, internal, external, mainOnly); + await refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly); } else if (currentView === 'tree') { - await refreshTree(depth, internal, external, mainOnly); + await refreshTree(depth, internal, external, moduleLevel, directOnly); } else { - await refreshList(internal, external, mainOnly); + await refreshList(internal, external, moduleLevel, directOnly); } } @@ -724,7 +734,7 @@ async function showPackageInfo(pkg) { } // Refresh graph view -async function refreshGraph(depth, group, reverse, internal, external, mainOnly) { +async function refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly) { document.getElementById('graphView').classList.remove('hidden'); document.getElementById('textView').classList.add('hidden'); @@ -738,8 +748,11 @@ async function refreshGraph(depth, group, reverse, internal, external, mainOnly) if (external !== undefined) { url += `&external=${external}`; } - if (mainOnly !== undefined) { - url += `&main=${mainOnly}`; + if (moduleLevel !== undefined) { + url += `&module=${moduleLevel}`; + } + if (directOnly !== undefined) { + url += `&direct=${directOnly}`; } if (selectedPackage) { url += '&package=' + encodeURIComponent(selectedPackage); @@ -815,7 +828,7 @@ function autoFitGraph() { } // Refresh tree view -async function refreshTree(depth, internal, external, mainOnly) { +async function refreshTree(depth, internal, external, moduleLevel, directOnly) { document.getElementById('graphView').classList.add('hidden'); document.getElementById('textView').classList.remove('hidden'); @@ -823,8 +836,11 @@ async function refreshTree(depth, internal, external, mainOnly) { if (external !== undefined) { url += `&external=${external}`; } - if (mainOnly !== undefined) { - url += `&main=${mainOnly}`; + if (moduleLevel !== undefined) { + url += `&module=${moduleLevel}`; + } + if (directOnly !== undefined) { + url += `&direct=${directOnly}`; } if (selectedPackage) { url += '&package=' + encodeURIComponent(selectedPackage); @@ -844,7 +860,7 @@ async function refreshTree(depth, internal, external, mainOnly) { } // Refresh list view -async function refreshList(internal, external, mainOnly) { +async function refreshList(internal, external, moduleLevel, directOnly) { document.getElementById('graphView').classList.add('hidden'); document.getElementById('textView').classList.remove('hidden'); @@ -852,8 +868,11 @@ async function refreshList(internal, external, mainOnly) { if (external !== undefined) { url += `&external=${external}`; } - if (mainOnly !== undefined) { - url += `&main=${mainOnly}`; + if (moduleLevel !== undefined) { + url += `&module=${moduleLevel}`; + } + if (directOnly !== undefined) { + url += `&direct=${directOnly}`; } if (selectedPackage) { url += '&package=' + encodeURIComponent(selectedPackage); diff --git a/cmd/gf/internal/cmd/cmddep/static/i18n.js b/cmd/gf/internal/cmd/cmddep/static/i18n.js index 3d11687f7..10cf3ea46 100644 --- a/cmd/gf/internal/cmd/cmddep/static/i18n.js +++ b/cmd/gf/internal/cmd/cmddep/static/i18n.js @@ -24,7 +24,8 @@ const i18n = { groupLabel: 'Group by directory', internalLabel: 'Internal only', externalLabel: 'External packages', - mainOnlyLabel: 'Main module only', + moduleLevelLabel: 'Module level', + directOnlyLabel: 'Direct only', statsInternal: 'Internal', statsExternal: 'External', statsStdlib: 'Stdlib', @@ -61,7 +62,8 @@ const i18n = { groupTooltip: 'Group packages by directory structure', internalTooltip: 'Show only internal packages from current module', externalTooltip: 'Include external dependency packages', - mainOnlyTooltip: 'Show only main module packages (exclude vendor)', + moduleLevelTooltip: 'Show module-level dependencies (from go.mod)', + directOnlyTooltip: 'Show only direct dependencies (requires module level)', layoutTDTooltip: 'Top-down layout for dependency graph', layoutLRTooltip: 'Left-right layout for dependency graph', showAllTooltip: 'Clear package selection and show all packages', @@ -100,7 +102,8 @@ const i18n = { groupLabel: '按目录分组', internalLabel: '仅内部包', externalLabel: '外部依赖包', - mainOnlyLabel: '仅主模块', + moduleLevelLabel: '模块级别', + directOnlyLabel: '仅直接依赖', statsInternal: '内部', statsExternal: '外部', statsStdlib: '标准库', @@ -137,7 +140,8 @@ const i18n = { groupTooltip: '按目录结构分组显示包', internalTooltip: '仅显示当前模块的内部包', externalTooltip: '包含外部依赖包', - mainOnlyTooltip: '仅显示主模块包(排除vendor目录)', + moduleLevelTooltip: '显示模块级别依赖(来自go.mod)', + directOnlyTooltip: '仅显示直接依赖(需要启用模块级别)', layoutTDTooltip: '依赖图从上到下布局', layoutLRTooltip: '依赖图从左到右布局', showAllTooltip: '清除包选择,显示所有包', diff --git a/cmd/gf/internal/cmd/cmddep/static/index.html b/cmd/gf/internal/cmd/cmddep/static/index.html index 1c52ea8b6..a9e10dfaf 100644 --- a/cmd/gf/internal/cmd/cmddep/static/index.html +++ b/cmd/gf/internal/cmd/cmddep/static/index.html @@ -79,8 +79,12 @@