// Copyright 2021 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build go1.21 // Command forks determines if Go modules are similar. package main import ( "cmp" "context" "encoding/gob" "errors" "flag" "fmt" "log" "os" "os/signal" "slices" "strings" "time" ) func main() { out := flag.CommandLine.Output() flag.Usage = func() { fmt.Fprintf(out, "usage: forks [ PATH | PATH@VERSION ] ...\n") fmt.Fprintf(out, "Print potential forks for each module path or module path @ version.\n") fmt.Fprintf(out, "Each fork is preceded by its score. Scores range from 0 to 10, with 10 meaning most\n") fmt.Fprintf(out, "similar. Only matches with scores of at least 6 are printed.\n") fmt.Fprintf(out, "Scores are approximations that are based on partial data (not the full module content),\n") fmt.Fprintf(out, "so even a score of 10 does not mean that the modules are identical.\n") flag.PrintDefaults() } flag.Parse() ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) if err := run(ctx); err != nil { log.Fatal(err) } } type Forks struct { Comment string Timestamp time.Time MinScore int // smallest score that is stored Modules []string // mapping from int to module@version Matches map[int][]Score // from module ID to matches and their scores } type Score struct { Module int // index into Forks.Modules Score int // 0 - 10 } func run(_ context.Context) error { if flag.NArg() == 0 { flag.Usage() return nil } dbFilename := os.Getenv("FORKSDB") if dbFilename == "" { return errors.New("must set FORKSDB to file") } f, err := os.Open(dbFilename) if err != nil { return err } defer f.Close() dec := gob.NewDecoder(f) var db Forks if err := dec.Decode(&db); err != nil { return err } // Build a map from path@version and path to ID. modsToIDs := buildIndex(db.Modules) // Print the forks for each arg. for _, arg := range flag.Args() { if ids, ok := modsToIDs[arg]; ok { for _, id := range ids { fmt.Printf("%s\n", db.Modules[id]) matches := db.Matches[id] slices.SortFunc(matches, func(s1, s2 Score) int { if c := cmp.Compare(s1.Score, s2.Score); c != 0 { return c } return cmp.Compare(db.Modules[s1.Module], db.Modules[s2.Module]) }) for _, m := range matches { fmt.Printf(" %2d %s\n", m.Score, db.Modules[m.Module]) } } } else { fmt.Printf("%s: no forks\n", arg) } } return nil } // buildIndex builds a map from "path@version" and "path" to IDs. func buildIndex(mods []string) map[string][]int { modsToIDs := map[string][]int{} for i, mv := range mods { path, _, found := strings.Cut(mv, "@") if !found { panic(fmt.Errorf("no '@' in %s", mv)) } modsToIDs[mv] = append(modsToIDs[mv], i) modsToIDs[path] = append(modsToIDs[path], i) } return modsToIDs }