blob: 21aca432845864b5a4bc78e6d430261bb65af990 [file] [log] [blame]
// 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
}