| // 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 | |
| } |