package main import ( "bufio" "context" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "regexp" "sort" "strings" "time" ) type Dependency struct { Name string Version string Description string License string LicenseURL string Homepage string } type GoModInfo struct { Version string `json:"Version"` Time string `json:"Time"` } type GitHubRepoInfo struct { Description string `json:"description"` License struct { Name string `json:"name"` SPDX string `json:"spdx_id"` Key string `json:"key"` } `json:"license"` Homepage string `json:"homepage"` HTMLURL string `json:"html_url"` } func Deps(targetFile string) error { fmt.Printf("Analyzing dependencies and generating report to %s\n", targetFile) deps, err := parseGoMod() if err != nil { return fmt.Errorf("failed to parse go.mod: %w", err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() if err := enrichDependencyInfo(ctx, deps); err != nil { return fmt.Errorf("failed to enrich dependency info: %w", err) } if err := generateDependencyReport(deps, targetFile); err != nil { return fmt.Errorf("failed to generate report: %w", err) } return nil } func parseGoMod() ([]Dependency, error) { file, err := os.Open("go.mod") if err != nil { return nil, err } defer file.Close() var deps []Dependency scanner := bufio.NewScanner(file) inRequireBlock := false for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "require (") { inRequireBlock = true continue } if inRequireBlock && line == ")" { inRequireBlock = false continue } if inRequireBlock || strings.HasPrefix(line, "require ") { if strings.Contains(line, "// indirect") { continue } // Clean up the line line = strings.TrimPrefix(line, "require ") line = strings.TrimSpace(line) // Skip comments and empty lines if strings.HasPrefix(line, "//") || line == "" { continue } // Parse module and version parts := strings.Fields(line) if len(parts) >= 2 { name := parts[0] version := parts[1] // Skip standard library and our own module if !strings.Contains(name, ".") || strings.HasPrefix(name, "github.com/pogo-vcs/pogo") { continue } deps = append(deps, Dependency{ Name: name, Version: version, }) } } } if err := scanner.Err(); err != nil { return nil, err } return deps, nil } func enrichDependencyInfo(ctx context.Context, deps []Dependency) error { client := &http.Client{ Timeout: 30 * time.Second, } for i := range deps { fmt.Printf("Fetching info for %s...\n", deps[i].Name) if err := fetchGoModuleInfo(ctx, client, &deps[i]); err != nil { fmt.Printf(" Warning: failed to fetch Go module info for %s: %v\n", deps[i].Name, err) } if err := fetchGitHubInfo(ctx, client, &deps[i]); err != nil { fmt.Printf(" Warning: failed to fetch GitHub info for %s: %v\n", deps[i].Name, err) // Try pkg.go.dev as fallback for non-GitHub packages if err := fetchPkgGoDevInfo(ctx, client, &deps[i]); err != nil { fmt.Printf(" Warning: failed to fetch pkg.go.dev info for %s: %v\n", deps[i].Name, err) } } // Small delay to be respectful to APIs time.Sleep(100 * time.Millisecond) } return nil } func fetchGoModuleInfo(ctx context.Context, client *http.Client, dep *Dependency) error { url := fmt.Sprintf("https://proxy.golang.org/%s/@v/%s.info", dep.Name, dep.Version) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("HTTP %d", resp.StatusCode) } var info GoModInfo if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return err } // The Go proxy doesn't provide description/license, so this is mainly for validation return nil } func fetchGitHubInfo(ctx context.Context, client *http.Client, dep *Dependency) error { // Extract GitHub repo from module path if !strings.HasPrefix(dep.Name, "github.com/") { return fmt.Errorf("not a GitHub module") } // Convert module path to GitHub API URL parts := strings.Split(dep.Name, "/") if len(parts) < 3 { return fmt.Errorf("invalid GitHub module path") } owner := parts[1] repo := parts[2] // Remove version suffixes like /v2, /v3 etc. if strings.HasPrefix(repo, "v") && len(repo) > 1 { // Check if this is a version suffix for _, char := range repo[1:] { if char < '0' || char > '9' { goto notversion } } // This is a version suffix, use the parent path if len(parts) > 3 { repo = parts[3] } } notversion: url := fmt.Sprintf("https://api.github.com/repos/%s/%s", owner, repo) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } // Add GitHub PAT if available if token := os.Getenv("GITHUB_TOKEN"); token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("HTTP %d", resp.StatusCode) } var repoInfo GitHubRepoInfo if err := json.NewDecoder(resp.Body).Decode(&repoInfo); err != nil { return err } dep.Description = repoInfo.Description if repoInfo.License.Name != "" { dep.License = repoInfo.License.Name } else if repoInfo.License.SPDX != "" { dep.License = repoInfo.License.SPDX } // Construct license URL from repository URL if repoInfo.License.Key != "" { dep.LicenseURL = repoInfo.HTMLURL + "/blob/main/LICENSE" } dep.Homepage = repoInfo.HTMLURL return nil } func fetchPkgGoDevInfo(ctx context.Context, client *http.Client, dep *Dependency) error { url := fmt.Sprintf("https://pkg.go.dev/%s", dep.Name) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("HTTP %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return err } content := string(body) // Extract license information licenseRegex := regexp.MustCompile(`License:\s*]*>([^<]+)`) if matches := licenseRegex.FindStringSubmatch(content); len(matches) > 1 { dep.License = strings.TrimSpace(matches[1]) // Construct license URL dep.LicenseURL = fmt.Sprintf("https://pkg.go.dev/%s?tab=licenses", dep.Name) } // Extract description from the overview section or readme overviewRegex := regexp.MustCompile(` 1 { description := strings.TrimSpace(matches[1]) // Clean up the description if strings.HasPrefix(description, "Package "+dep.Name) { description = strings.TrimPrefix(description, "Package "+dep.Name) description = strings.TrimSpace(description) } if description != "" && dep.Description == "" { dep.Description = description } } // Set homepage to pkg.go.dev if not already set if dep.Homepage == "" { dep.Homepage = url } return nil } func generateDependencyReport(deps []Dependency, targetFile string) error { // Sort dependencies by name sort.Slice(deps, func(i, j int) bool { return deps[i].Name < deps[j].Name }) // Ensure target directory exists if err := os.MkdirAll(filepath.Dir(targetFile), 0755); err != nil { return err } file, err := os.Create(targetFile) if err != nil { return err } defer file.Close() w := bufio.NewWriter(file) defer w.Flush() // Write header _, _ = fmt.Fprintf(w, "---\ntitle: Dependencies\ndescription: Pogo's first party Dependencies\n---\n\n") _, _ = fmt.Fprintf(w, "This document lists all first-party dependencies used by Pogo.\n\n") _, _ = fmt.Fprintf(w, "Generated on: %s\n\n", time.Now().Format("2006-01-02 15:04:05")) // Write summary _, _ = fmt.Fprintf(w, "## Summary\n\n") _, _ = fmt.Fprintf(w, "Total dependencies: %d\n\n", len(deps)) // Count licenses licenses := make(map[string]int) for _, dep := range deps { if dep.License != "" { licenses[dep.License]++ } else { licenses["Unknown"]++ } } _, _ = fmt.Fprintf(w, "### License Distribution\n\n") for license, count := range licenses { _, _ = fmt.Fprintf(w, "- %s: %d\n", license, count) } _, _ = fmt.Fprintf(w, "\n") // Write detailed dependency list _, _ = fmt.Fprintf(w, "## Dependencies\n\n") for _, dep := range deps { _, _ = fmt.Fprintf(w, "### %s\n\n", dep.Name) _, _ = fmt.Fprintf(w, "**Version:** %s\n\n", dep.Version) if dep.Description != "" { _, _ = fmt.Fprintf(w, "**Description:** %s\n\n", dep.Description) } if dep.License != "" { if dep.LicenseURL != "" { _, _ = fmt.Fprintf(w, "**License:** [%s](%s)\n\n", dep.License, dep.LicenseURL) } else { _, _ = fmt.Fprintf(w, "**License:** %s\n\n", dep.License) } } else { _, _ = fmt.Fprintf(w, "**License:** Unknown\n\n") } if dep.Homepage != "" { _, _ = fmt.Fprintf(w, "**Homepage:** %s\n\n", dep.Homepage) } _, _ = fmt.Fprintf(w, "---\n\n") } return nil }