package validation import ( "context" "fmt" "path" "strings" "gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "golang.org/x/mod/modfile" ) func checkLocalReplacements(ctx context.Context, tree *CandidateTree) error { var findings []Finding replacementsByObjectID := make(map[string][]moduleReplacementSummary) for _, entry := range tree.inputEntries { if err := ctx.Err(); err != nil { return &Failure{Check: LocalReplacements, Findings: findings, Cause: err} } if path.Base(entry.Path) != "go.mod" { continue } declaration, err := resolveModuleDeclaration(ctx, tree, entry) if err != nil { return &Failure{ Check: LocalReplacements, Findings: []Finding{{Path: entry.Path, Message: "could not inspect committed module declaration"}}, Cause: fmt.Errorf("inspect committed %s: %w", entry.Path, err), } } replacements, ok := replacementsByObjectID[declaration.ObjectID] if !ok { contents, err := tree.ReadFile(ctx, declaration.Path) if err != nil { return &Failure{ Check: LocalReplacements, Findings: []Finding{{Path: entry.Path, Message: "could not inspect committed module declaration"}}, Cause: fmt.Errorf("inspect committed %s: read module declaration: %w", entry.Path, err), } } moduleFile, err := modfile.Parse(entry.Path, contents, nil) if err != nil { return &Failure{ Check: LocalReplacements, Findings: []Finding{{Path: entry.Path, Message: "could not parse committed module declaration"}}, Cause: fmt.Errorf("parse committed %s: %w", entry.Path, err), } } replacements = make([]moduleReplacementSummary, 0, len(moduleFile.Replace)) for _, replacement := range moduleFile.Replace { if replacement.New.Version != "" { continue } line := 0 if replacement.Syntax != nil { line = replacement.Syntax.Start.Line } replacements = append(replacements, moduleReplacementSummary{ target: strings.Clone(replacement.New.Path), line: line, }) } contents = nil moduleFile = nil replacementsByObjectID[declaration.ObjectID] = replacements } for _, replacement := range replacements { finding := Finding{ Path: entry.Path, Message: "filesystem module replacement is committed", Target: replacement.target, Directive: "replace", Line: replacement.line, } findings = append(findings, finding) } } if len(findings) != 0 { return &Failure{Check: LocalReplacements, Findings: findings} } return nil } type moduleReplacementSummary struct { target string line int } func resolveModuleDeclaration(ctx context.Context, tree *CandidateTree, entry git.TreeEntry) (git.TreeEntry, error) { if entry.Type != "blob" { return git.TreeEntry{}, fmt.Errorf("entry has type %q, not a blob", entry.Type) } if entry.Mode != "120000" { if entry.Mode != "100644" && entry.Mode != "100755" { return git.TreeEntry{}, fmt.Errorf("entry has unsupported mode %q", entry.Mode) } return entry, nil } target, err := tree.readSymlinkTarget(ctx, entry) if err != nil { return git.TreeEntry{}, fmt.Errorf("read symlink declaration: %w", err) } destination, err := tree.ResolveDestination(ctx, entry.Path, target) if err != nil { return git.TreeEntry{}, fmt.Errorf("resolve symlink declaration target %q: %w", target, err) } if destination.Kind != DestinationEntry { return git.TreeEntry{}, fmt.Errorf("symlink declaration target %q resolves to %s", target, destination.Kind) } resolved, ok := tree.Entry(destination.Path) if !ok || resolved.Type != "blob" || (resolved.Mode != "100644" && resolved.Mode != "100755") { return git.TreeEntry{}, fmt.Errorf("symlink declaration target %q does not resolve to a regular committed file", target) } return resolved, nil }