blob: 1d00865eed011c6d46647425577247c1d0af9382 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package cli
import (
"context"
"flag"
"path/filepath"
"strings"
"go.chromium.org/luci/common/data/text"
"go.chromium.org/luci/common/errors"
"infra/rts/filegraph"
"infra/rts/filegraph/git"
"infra/rts/internal/gitutil"
)
// gitGraph loads a file graph from a git log.
type gitGraph struct {
opt git.LoadOptions
edgeReader git.EdgeReader
maxDistance float64
*git.Graph
}
func (g *gitGraph) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(&g.opt.Ref, "ref", "refs/heads/main", text.Doc(`
Load the file graph for this git ref.
For refs/heads/main, refs/heads/master is read if main doesn't exist.
`))
fs.IntVar(&g.opt.MaxCommitSize, "max-commit-size", 100, text.Doc(`
Maximum number of files touched by a commit.
Commits that exceed this limit are ignored.
The rationale is that large commits provide a weak signal of file
relatedness and are expensive to process, O(N^2).
`))
fs.Float64Var(&g.maxDistance, "max-distance", 0, text.Doc(`
If positive, the distance threshold. Nodes further than this are considered
unreachable.
`))
}
func (g *gitGraph) query(sources ...filegraph.Node) *filegraph.Query {
return &filegraph.Query{
Sources: sources,
EdgeReader: &g.edgeReader,
MaxDistance: g.maxDistance,
}
}
func (g *gitGraph) Validate() error {
if !strings.HasPrefix(g.opt.Ref, "refs/") {
return errors.Reason("-ref %q doesn't start with refs/", g.opt.Ref).Err()
}
if g.opt.MaxCommitSize < 0 {
return errors.Reason("-max-commit-size must be non-negative").Err()
}
return nil
}
// loadSyncedNodes calls loadSyncedGraph for filePaths' repo, and then loads a
// node for each of the files.
func (g *gitGraph) loadSyncedNodes(ctx context.Context, filePaths ...string) ([]filegraph.Node, error) {
repoDir, err := gitutil.EnsureSameRepo(filePaths...)
if err != nil {
return nil, err
}
// Load the graph.
if g.Graph, err = git.Load(ctx, repoDir, g.opt); err != nil {
return nil, err
}
// Load the nodes.
nodes := make([]filegraph.Node, len(filePaths))
for i, f := range filePaths {
// Convert the filename to a node name.
if f, err = filepath.Abs(f); err != nil {
return nil, err
}
name, err := filepath.Rel(repoDir, f)
if err != nil {
return nil, err
}
name = filepath.ToSlash(name)
switch {
case name == ".":
name = "//" // the root
case strings.HasPrefix(name, "/") || strings.HasPrefix(name, "../") || strings.HasPrefix(name, "./"):
return nil, errors.Reason("unexpected path %q", name).Err()
default:
name = "//" + name
}
// Load the node.
node := g.Node(name)
if node == nil {
return nil, errors.Reason("node %q not found", name).Err()
}
nodes[i] = node
}
return nodes, nil
}